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_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, 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        execution::TradingCommand,
76        system::{QueueStateChanged, SocketStateChanged},
77    },
78    msgbus::{self, ShareableMessageHandler},
79    python::{
80        cache::PyCache,
81        clock::PyClock,
82        indicators::{registered_python_indicators, wrap_python_indicator},
83        logging::{PyLogger, format_exception},
84        wrappers::{get_python_message_bus, retain_python_wrapper},
85    },
86    runner::SystemChannel,
87    signal::Signal,
88    timer::{TimeEvent, TimeEventCallback},
89};
90
91#[pyo3::pymethods]
92#[pyo3_stub_gen::derive::gen_stub_pymethods]
93impl DataActorConfig {
94    /// Common configuration for `DataActor` based components.
95    #[new]
96    #[pyo3(signature = (actor_id=None, log_events=true, log_commands=true, **_kwargs))]
97    fn py_new(
98        actor_id: Option<ActorId>,
99        log_events: bool,
100        log_commands: bool,
101        _kwargs: Option<&Bound<'_, PyDict>>,
102    ) -> Self {
103        Self {
104            actor_id,
105            log_events,
106            log_commands,
107        }
108    }
109
110    #[getter]
111    fn actor_id(&self) -> Option<ActorId> {
112        self.actor_id
113    }
114
115    #[setter]
116    fn set_actor_id(&mut self, actor_id: Option<ActorId>) {
117        self.actor_id = actor_id;
118    }
119
120    #[getter]
121    fn log_events(&self) -> bool {
122        self.log_events
123    }
124
125    #[setter]
126    fn set_log_events(&mut self, log_events: bool) {
127        self.log_events = log_events;
128    }
129
130    #[getter]
131    fn log_commands(&self) -> bool {
132        self.log_commands
133    }
134
135    #[setter]
136    fn set_log_commands(&mut self, log_commands: bool) {
137        self.log_commands = log_commands;
138    }
139}
140
141#[pyo3::pymethods]
142#[pyo3_stub_gen::derive::gen_stub_pymethods]
143impl ImportableActorConfig {
144    /// Configuration for creating actors from importable paths.
145    #[new]
146    #[expect(clippy::needless_pass_by_value)]
147    fn py_new(actor_path: String, config_path: String, config: Py<PyDict>) -> PyResult<Self> {
148        let json_config = Python::attach(|py| -> PyResult<HashMap<String, serde_json::Value>> {
149            let kwargs = PyDict::new(py);
150            kwargs.set_item("default", py.eval(pyo3::ffi::c_str!("str"), None, None)?)?;
151            let json_str: String = PyModule::import(py, "json")?
152                .call_method("dumps", (config.bind(py),), Some(&kwargs))?
153                .extract()?;
154
155            let json_value: serde_json::Value =
156                serde_json::from_str(&json_str).map_err(to_pyvalue_err)?;
157
158            if let serde_json::Value::Object(map) = json_value {
159                Ok(map.into_iter().collect())
160            } else {
161                Err(to_pyvalue_err("Config must be a dictionary"))
162            }
163        })?;
164
165        Ok(Self {
166            actor_path,
167            config_path,
168            config: json_config,
169        })
170    }
171
172    #[getter]
173    fn actor_path(&self) -> &String {
174        &self.actor_path
175    }
176
177    #[getter]
178    fn config_path(&self) -> &String {
179        &self.config_path
180    }
181
182    #[getter]
183    fn config(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
184        // Convert HashMap<String, serde_json::Value> back to Python dict
185        let py_dict = PyDict::new(py);
186
187        for (key, value) in &self.config {
188            // Convert serde_json::Value back to Python object via JSON
189            let json_str = serde_json::to_string(value).map_err(to_pyvalue_err)?;
190            let py_value = PyModule::import(py, "json")?.call_method("loads", (json_str,), None)?;
191            py_dict.set_item(key, py_value)?;
192        }
193        Ok(py_dict.unbind())
194    }
195}
196
197/// Inner state of `PyDataActor`, shared between Python wrapper and Rust registries.
198///
199/// This type holds the actual actor state and implements all the actor traits.
200/// It is wrapped in `Rc<UnsafeCell<>>` to allow shared ownership between Python
201/// and the global registries without copying.
202pub struct PyDataActorInner {
203    core: DataActorCore,
204    py_self: Option<Py<PyWeakrefReference>>,
205    config: Option<Py<PyAny>>,
206    clock: PyClock,
207    logger: PyLogger,
208}
209
210impl Debug for PyDataActorInner {
211    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212        f.debug_struct(stringify!(PyDataActorInner))
213            .field("core", &self.core)
214            .field(
215                "py_self",
216                &self.py_self.as_ref().map(|_| "<Py<PyWeakrefReference>>"),
217            )
218            .field("config", &self.config.as_ref().map(|_| "<Py<PyAny>>"))
219            .field("clock", &self.clock)
220            .field("logger", &self.logger)
221            .finish()
222    }
223}
224
225impl Deref for PyDataActorInner {
226    type Target = DataActorCore;
227
228    fn deref(&self) -> &Self::Target {
229        &self.core
230    }
231}
232
233impl DerefMut for PyDataActorInner {
234    fn deref_mut(&mut self) -> &mut Self::Target {
235        &mut self.core
236    }
237}
238
239impl DataActorNative for PyDataActorInner {
240    fn core(&self) -> &DataActorCore {
241        &self.core
242    }
243
244    fn core_mut(&mut self) -> &mut DataActorCore {
245        &mut self.core
246    }
247}
248
249#[expect(clippy::needless_pass_by_ref_mut)]
250impl PyDataActorInner {
251    fn execute_exec_algorithm_command(&mut self, command: &TradingCommand) -> anyhow::Result<()> {
252        if self.core.config.log_commands {
253            let id = self.core.actor_id;
254            log::info!("{id} {RECV}{CMD} {command}");
255        }
256
257        if self.core.state() != ComponentState::Running {
258            return Ok(());
259        }
260
261        match command {
262            TradingCommand::SubmitOrder(cmd) => {
263                let order = DataActor::cache(self).try_order(&cmd.client_order_id)?;
264                self.dispatch_on_order(order).map_err(|e| {
265                    anyhow::anyhow!("Python on_order failed:\n{}", format_exception(&e))
266                })
267            }
268            TradingCommand::SubmitOrderList(cmd) => {
269                let orders = self.orders_for_list(&cmd.order_list)?;
270                self.dispatch_on_order_list(cmd.order_list.clone(), orders)
271                    .map_err(|e| {
272                        anyhow::anyhow!("Python on_order_list failed:\n{}", format_exception(&e))
273                    })
274            }
275            _ => {
276                log::warn!("Unhandled command type: {command}");
277                Ok(())
278            }
279        }
280    }
281
282    fn orders_for_list(&self, order_list: &OrderList) -> anyhow::Result<Vec<OrderAny>> {
283        let cache = DataActor::cache(self);
284        let mut orders = Vec::with_capacity(order_list.client_order_ids.len());
285
286        for client_order_id in &order_list.client_order_ids {
287            orders.push(cache.try_order(client_order_id)?);
288        }
289
290        Ok(orders)
291    }
292
293    fn dispatch_on_order(&mut self, order: OrderAny) -> PyResult<()> {
294        if let Some(py_self) = self.python_instance()? {
295            Python::attach(|py| -> PyResult<()> {
296                let py_order = order_any_to_pyobject(py, order)?;
297                py_self.call_method1(py, "on_order", (py_order,))?;
298                Ok(())
299            })?;
300        }
301        Ok(())
302    }
303
304    fn dispatch_on_order_list(
305        &mut self,
306        order_list: OrderList,
307        orders: Vec<OrderAny>,
308    ) -> PyResult<()> {
309        if let Some(py_self) = self.python_instance()? {
310            Python::attach(|py| -> PyResult<()> {
311                if py_self.bind(py).hasattr("on_order_list")? {
312                    let py_order_list = order_list.into_py_any(py)?;
313                    let py_orders = orders
314                        .into_iter()
315                        .map(|order| order_any_to_pyobject(py, order))
316                        .collect::<PyResult<Vec<_>>>()?;
317                    let py_orders = PyList::new(py, py_orders)?;
318
319                    py_self.call_method1(py, "on_order_list", (py_order_list, py_orders))?;
320                } else {
321                    for order in orders {
322                        let py_order = order_any_to_pyobject(py, order)?;
323                        py_self.call_method1(py, "on_order", (py_order,))?;
324                    }
325                }
326                Ok(())
327            })?;
328        }
329        Ok(())
330    }
331
332    fn dispatch_on_start(&self) -> PyResult<()> {
333        if let Some(py_self) = self.python_instance()? {
334            Python::attach(|py| py_self.call_method0(py, "on_start"))?;
335        }
336        Ok(())
337    }
338
339    fn dispatch_on_stop(&mut self) -> PyResult<()> {
340        if let Some(py_self) = self.python_instance()? {
341            Python::attach(|py| py_self.call_method0(py, "on_stop"))?;
342        }
343        Ok(())
344    }
345
346    fn dispatch_on_resume(&mut self) -> PyResult<()> {
347        if let Some(py_self) = self.python_instance()? {
348            Python::attach(|py| py_self.call_method0(py, "on_resume"))?;
349        }
350        Ok(())
351    }
352
353    fn dispatch_on_reset(&mut self) -> PyResult<()> {
354        if let Some(py_self) = self.python_instance()? {
355            Python::attach(|py| py_self.call_method0(py, "on_reset"))?;
356        }
357        Ok(())
358    }
359
360    fn dispatch_on_dispose(&mut self) -> PyResult<()> {
361        if let Some(py_self) = self.python_instance()? {
362            Python::attach(|py| py_self.call_method0(py, "on_dispose"))?;
363        }
364        Ok(())
365    }
366
367    fn dispatch_on_degrade(&mut self) -> PyResult<()> {
368        if let Some(py_self) = self.python_instance()? {
369            Python::attach(|py| py_self.call_method0(py, "on_degrade"))?;
370        }
371        Ok(())
372    }
373
374    fn dispatch_on_fault(&mut self) -> PyResult<()> {
375        if let Some(py_self) = self.python_instance()? {
376            Python::attach(|py| py_self.call_method0(py, "on_fault"))?;
377        }
378        Ok(())
379    }
380
381    fn dispatch_on_save(&self) -> PyResult<IndexMap<String, Vec<u8>>> {
382        if let Some(py_self) = self.python_instance()? {
383            Python::attach(|py| {
384                let py_state = py_self.call_method0(py, "on_save")?;
385                let py_state: &Bound<'_, PyDict> = py_state.cast_bound::<PyDict>(py)?;
386                pydict_to_state(py_state)
387            })
388        } else {
389            Ok(IndexMap::new())
390        }
391    }
392
393    fn dispatch_on_load(&mut self, state: &IndexMap<String, Vec<u8>>) -> PyResult<()> {
394        if let Some(py_self) = self.python_instance()? {
395            Python::attach(|py| -> PyResult<()> {
396                let py_state = state_to_pydict(py, state)?;
397                py_self.call_method1(py, "on_load", (py_state,))?;
398                Ok(())
399            })?;
400        }
401        Ok(())
402    }
403
404    fn dispatch_on_time_event(&mut self, event: TimeEvent) -> PyResult<()> {
405        if let Some(py_self) = self.python_instance()? {
406            Python::attach(|py| {
407                py_self.call_method1(py, "on_time_event", (event.into_py_any(py)?,))
408            })?;
409        }
410        Ok(())
411    }
412
413    fn dispatch_on_data(&mut self, data: Py<PyAny>) -> PyResult<()> {
414        if let Some(py_self) = self.python_instance()? {
415            Python::attach(|py| py_self.call_method1(py, "on_data", (data,)))?;
416        }
417        Ok(())
418    }
419
420    fn dispatch_on_signal(&mut self, signal: &Signal) -> PyResult<()> {
421        if let Some(py_self) = self.python_instance()? {
422            Python::attach(|py| {
423                py_self.call_method1(py, "on_signal", (signal.clone().into_py_any(py)?,))
424            })?;
425        }
426        Ok(())
427    }
428
429    fn dispatch_on_queue_state(&mut self, event: &QueueStateChanged) -> PyResult<()> {
430        if let Some(py_self) = self.python_instance()? {
431            Python::attach(|py| {
432                py_self.call_method1(py, "on_queue_state", (event.clone().into_py_any(py)?,))
433            })?;
434        }
435        Ok(())
436    }
437
438    fn dispatch_on_socket_state(&mut self, event: &SocketStateChanged) -> PyResult<()> {
439        if let Some(py_self) = self.python_instance()? {
440            Python::attach(|py| {
441                py_self.call_method1(py, "on_socket_state", (event.clone().into_py_any(py)?,))
442            })?;
443        }
444        Ok(())
445    }
446
447    fn dispatch_on_instrument(&mut self, instrument: Py<PyAny>) -> PyResult<()> {
448        if let Some(py_self) = self.python_instance()? {
449            Python::attach(|py| py_self.call_method1(py, "on_instrument", (instrument,)))?;
450        }
451        Ok(())
452    }
453
454    fn dispatch_on_quote(&mut self, quote: QuoteTick) -> PyResult<()> {
455        if let Some(py_self) = self.python_instance()? {
456            Python::attach(|py| py_self.call_method1(py, "on_quote", (quote.into_py_any(py)?,)))?;
457        }
458        Ok(())
459    }
460
461    fn dispatch_on_trade(&mut self, trade: TradeTick) -> PyResult<()> {
462        if let Some(py_self) = self.python_instance()? {
463            Python::attach(|py| py_self.call_method1(py, "on_trade", (trade.into_py_any(py)?,)))?;
464        }
465        Ok(())
466    }
467
468    fn dispatch_on_bar(&mut self, bar: Bar) -> PyResult<()> {
469        if let Some(py_self) = self.python_instance()? {
470            Python::attach(|py| py_self.call_method1(py, "on_bar", (bar.into_py_any(py)?,)))?;
471        }
472        Ok(())
473    }
474
475    fn dispatch_on_book_deltas(&mut self, deltas: OrderBookDeltas) -> PyResult<()> {
476        if let Some(py_self) = self.python_instance()? {
477            Python::attach(|py| {
478                py_self.call_method1(py, "on_book_deltas", (deltas.into_py_any(py)?,))
479            })?;
480        }
481        Ok(())
482    }
483
484    fn dispatch_on_book_depth(&mut self, depth: &OrderBookDepth) -> PyResult<()> {
485        if let Some(py_self) = self.python_instance()? {
486            Python::attach(|py| {
487                py_self.call_method1(py, "on_book_depth", (depth.clone().into_py_any(py)?,))
488            })?;
489        }
490        Ok(())
491    }
492
493    fn dispatch_on_book(&mut self, book: &OrderBook) -> PyResult<()> {
494        if let Some(py_self) = self.python_instance()? {
495            Python::attach(|py| {
496                py_self.call_method1(py, "on_book", (book.clone().into_py_any(py)?,))
497            })?;
498        }
499        Ok(())
500    }
501
502    fn dispatch_on_mark_price(&mut self, mark_price: MarkPriceUpdate) -> PyResult<()> {
503        if let Some(py_self) = self.python_instance()? {
504            Python::attach(|py| {
505                py_self.call_method1(py, "on_mark_price", (mark_price.into_py_any(py)?,))
506            })?;
507        }
508        Ok(())
509    }
510
511    fn dispatch_on_index_price(&mut self, index_price: IndexPriceUpdate) -> PyResult<()> {
512        if let Some(py_self) = self.python_instance()? {
513            Python::attach(|py| {
514                py_self.call_method1(py, "on_index_price", (index_price.into_py_any(py)?,))
515            })?;
516        }
517        Ok(())
518    }
519
520    fn dispatch_on_funding_rate(&mut self, funding_rate: FundingRateUpdate) -> PyResult<()> {
521        if let Some(py_self) = self.python_instance()? {
522            Python::attach(|py| {
523                py_self.call_method1(py, "on_funding_rate", (funding_rate.into_py_any(py)?,))
524            })?;
525        }
526        Ok(())
527    }
528
529    fn dispatch_on_instrument_status(&mut self, data: InstrumentStatus) -> PyResult<()> {
530        if let Some(py_self) = self.python_instance()? {
531            Python::attach(|py| {
532                py_self.call_method1(py, "on_instrument_status", (data.into_py_any(py)?,))
533            })?;
534        }
535        Ok(())
536    }
537
538    fn dispatch_on_instrument_close(&mut self, update: InstrumentClose) -> PyResult<()> {
539        if let Some(py_self) = self.python_instance()? {
540            Python::attach(|py| {
541                py_self.call_method1(py, "on_instrument_close", (update.into_py_any(py)?,))
542            })?;
543        }
544        Ok(())
545    }
546
547    fn dispatch_on_option_greeks(&mut self, greeks: OptionGreeks) -> PyResult<()> {
548        if let Some(py_self) = self.python_instance()? {
549            Python::attach(|py| {
550                py_self.call_method1(py, "on_option_greeks", (greeks.into_py_any(py)?,))
551            })?;
552        }
553        Ok(())
554    }
555
556    fn dispatch_on_option_chain(&mut self, slice: OptionChainSlice) -> PyResult<()> {
557        if let Some(py_self) = self.python_instance()? {
558            Python::attach(|py| {
559                py_self.call_method1(py, "on_option_chain", (slice.into_py_any(py)?,))
560            })?;
561        }
562        Ok(())
563    }
564
565    fn dispatch_on_historical_data(&mut self, data: Py<PyAny>) -> PyResult<()> {
566        if let Some(py_self) = self.python_instance()? {
567            Python::attach(|py| py_self.call_method1(py, "on_historical_data", (data,)))?;
568        }
569        Ok(())
570    }
571
572    fn dispatch_on_historical_book_deltas(&mut self, deltas: Vec<OrderBookDelta>) -> PyResult<()> {
573        if let Some(py_self) = self.python_instance()? {
574            Python::attach(|py| {
575                let py_deltas = deltas
576                    .into_iter()
577                    .map(|delta| delta.into_py_any(py))
578                    .collect::<PyResult<Vec<_>>>()?;
579                py_self.call_method1(py, "on_historical_book_deltas", (py_deltas,))
580            })?;
581        }
582        Ok(())
583    }
584
585    fn dispatch_on_historical_book_depth(&mut self, depths: Vec<OrderBookDepth>) -> PyResult<()> {
586        if let Some(py_self) = self.python_instance()? {
587            Python::attach(|py| {
588                let py_depths = depths
589                    .into_iter()
590                    .map(|depth| depth.into_py_any(py))
591                    .collect::<PyResult<Vec<_>>>()?;
592                py_self.call_method1(py, "on_historical_book_depth", (py_depths,))
593            })?;
594        }
595        Ok(())
596    }
597
598    fn dispatch_on_historical_quotes(&mut self, quotes: Vec<QuoteTick>) -> PyResult<()> {
599        if let Some(py_self) = self.python_instance()? {
600            Python::attach(|py| {
601                let py_quotes = quotes
602                    .into_iter()
603                    .map(|q| q.into_py_any(py))
604                    .collect::<PyResult<Vec<_>>>()?;
605                py_self.call_method1(py, "on_historical_quotes", (py_quotes,))
606            })?;
607        }
608        Ok(())
609    }
610
611    fn dispatch_on_historical_trades(&mut self, trades: Vec<TradeTick>) -> PyResult<()> {
612        if let Some(py_self) = self.python_instance()? {
613            Python::attach(|py| {
614                let py_trades = trades
615                    .into_iter()
616                    .map(|t| t.into_py_any(py))
617                    .collect::<PyResult<Vec<_>>>()?;
618                py_self.call_method1(py, "on_historical_trades", (py_trades,))
619            })?;
620        }
621        Ok(())
622    }
623
624    fn dispatch_on_historical_funding_rates(
625        &mut self,
626        funding_rates: Vec<FundingRateUpdate>,
627    ) -> PyResult<()> {
628        if let Some(py_self) = self.python_instance()? {
629            Python::attach(|py| {
630                let py_rates = funding_rates
631                    .into_iter()
632                    .map(|r| r.into_py_any(py))
633                    .collect::<PyResult<Vec<_>>>()?;
634                py_self.call_method1(py, "on_historical_funding_rates", (py_rates,))
635            })?;
636        }
637        Ok(())
638    }
639
640    fn dispatch_on_historical_bars(&mut self, bars: Vec<Bar>) -> PyResult<()> {
641        if let Some(py_self) = self.python_instance()? {
642            Python::attach(|py| {
643                let py_bars = bars
644                    .into_iter()
645                    .map(|b| b.into_py_any(py))
646                    .collect::<PyResult<Vec<_>>>()?;
647                py_self.call_method1(py, "on_historical_bars", (py_bars,))
648            })?;
649        }
650        Ok(())
651    }
652
653    fn dispatch_on_historical_mark_prices(
654        &mut self,
655        mark_prices: Vec<MarkPriceUpdate>,
656    ) -> PyResult<()> {
657        if let Some(py_self) = self.python_instance()? {
658            Python::attach(|py| {
659                let py_prices = mark_prices
660                    .into_iter()
661                    .map(|p| p.into_py_any(py))
662                    .collect::<PyResult<Vec<_>>>()?;
663                py_self.call_method1(py, "on_historical_mark_prices", (py_prices,))
664            })?;
665        }
666        Ok(())
667    }
668
669    fn dispatch_on_historical_index_prices(
670        &mut self,
671        index_prices: Vec<IndexPriceUpdate>,
672    ) -> PyResult<()> {
673        if let Some(py_self) = self.python_instance()? {
674            Python::attach(|py| {
675                let py_prices = index_prices
676                    .into_iter()
677                    .map(|p| p.into_py_any(py))
678                    .collect::<PyResult<Vec<_>>>()?;
679                py_self.call_method1(py, "on_historical_index_prices", (py_prices,))
680            })?;
681        }
682        Ok(())
683    }
684
685    #[cfg(feature = "defi")]
686    fn dispatch_on_block(&mut self, block: Block) -> PyResult<()> {
687        if let Some(py_self) = self.python_instance()? {
688            Python::attach(|py| py_self.call_method1(py, "on_block", (block.into_py_any(py)?,)))?;
689        }
690        Ok(())
691    }
692
693    #[cfg(feature = "defi")]
694    fn dispatch_on_pool(&mut self, pool: Pool) -> PyResult<()> {
695        if let Some(py_self) = self.python_instance()? {
696            Python::attach(|py| py_self.call_method1(py, "on_pool", (pool.into_py_any(py)?,)))?;
697        }
698        Ok(())
699    }
700
701    #[cfg(feature = "defi")]
702    fn dispatch_on_pool_swap(&mut self, swap: PoolSwap) -> PyResult<()> {
703        if let Some(py_self) = self.python_instance()? {
704            Python::attach(|py| {
705                py_self.call_method1(py, "on_pool_swap", (swap.into_py_any(py)?,))
706            })?;
707        }
708        Ok(())
709    }
710
711    #[cfg(feature = "defi")]
712    fn dispatch_on_pool_liquidity_update(&mut self, update: PoolLiquidityUpdate) -> PyResult<()> {
713        if let Some(py_self) = self.python_instance()? {
714            Python::attach(|py| {
715                py_self.call_method1(py, "on_pool_liquidity_update", (update.into_py_any(py)?,))
716            })?;
717        }
718        Ok(())
719    }
720
721    #[cfg(feature = "defi")]
722    fn dispatch_on_pool_fee_collect(&mut self, collect: PoolFeeCollect) -> PyResult<()> {
723        if let Some(py_self) = self.python_instance()? {
724            Python::attach(|py| {
725                py_self.call_method1(py, "on_pool_fee_collect", (collect.into_py_any(py)?,))
726            })?;
727        }
728        Ok(())
729    }
730
731    #[cfg(feature = "defi")]
732    fn dispatch_on_pool_flash(&mut self, flash: PoolFlash) -> PyResult<()> {
733        if let Some(py_self) = self.python_instance()? {
734            Python::attach(|py| {
735                py_self.call_method1(py, "on_pool_flash", (flash.into_py_any(py)?,))
736            })?;
737        }
738        Ok(())
739    }
740
741    // The trader owns the wrapper for as long as the actor stays registered, so a collected
742    // wrapper propagates as an error rather than a skipped callback.
743    fn python_instance(&self) -> PyResult<Option<Py<PyAny>>> {
744        upgrade_py_weakref(self.py_self.as_ref(), &self.core.actor_id)
745    }
746}
747
748fn dict_to_params(
749    py: Python<'_>,
750    params: Option<Py<PyDict>>,
751) -> PyResult<Option<nautilus_core::Params>> {
752    match params {
753        Some(dict) => from_pydict(py, &dict),
754        None => Ok(None),
755    }
756}
757
758fn state_to_pydict(py: Python<'_>, state: &IndexMap<String, Vec<u8>>) -> PyResult<Py<PyDict>> {
759    let py_state = PyDict::new(py);
760    for (key, value) in state {
761        py_state.set_item(key, PyBytes::new(py, value))?;
762    }
763    Ok(py_state.unbind())
764}
765
766fn pydict_to_state(state: &Bound<'_, PyDict>) -> PyResult<IndexMap<String, Vec<u8>>> {
767    let mut rust_state = IndexMap::with_capacity(state.len());
768    for (key, value) in state.iter() {
769        rust_state.insert(key.extract()?, value.extract()?);
770    }
771    Ok(rust_state)
772}
773
774/// Python-facing wrapper for `DataActor`.
775///
776/// This wrapper holds shared ownership of `PyDataActorInner` via `Rc<UnsafeCell<>>`.
777/// Both Python (through this wrapper) and the global registries share the same
778/// underlying actor instance, ensuring mutations are visible from both sides.
779#[allow(non_camel_case_types)]
780#[pyo3::pyclass(
781    module = "nautilus_trader.common",
782    name = "DataActor",
783    unsendable,
784    subclass,
785    weakref
786)]
787#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")]
788pub struct PyDataActor {
789    inner: Rc<UnsafeCell<PyDataActorInner>>,
790}
791
792impl Debug for PyDataActor {
793    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
794        f.debug_struct(stringify!(PyDataActor))
795            .field("inner", &self.inner())
796            .finish()
797    }
798}
799
800impl PyDataActor {
801    /// Returns a reference to the inner actor state.
802    ///
803    /// # Safety
804    ///
805    /// This is safe for single-threaded use. The `UnsafeCell` allows interior
806    /// mutability which is required for the registries to mutate the actor.
807    #[inline]
808    #[allow(unsafe_code)]
809    pub(crate) fn inner(&self) -> &PyDataActorInner {
810        unsafe { &*self.inner.get() }
811    }
812
813    /// Returns a mutable reference to the inner actor state.
814    ///
815    /// # Safety
816    ///
817    /// This is safe for single-threaded use. Callers must ensure no aliasing
818    /// mutable references exist.
819    #[inline]
820    #[allow(unsafe_code, clippy::mut_from_ref)]
821    pub(crate) fn inner_mut(&self) -> &mut PyDataActorInner {
822        unsafe { &mut *self.inner.get() }
823    }
824}
825
826impl Deref for PyDataActor {
827    type Target = DataActorCore;
828
829    fn deref(&self) -> &Self::Target {
830        &self.inner().core
831    }
832}
833
834impl DerefMut for PyDataActor {
835    fn deref_mut(&mut self) -> &mut Self::Target {
836        &mut self.inner_mut().core
837    }
838}
839
840impl PyDataActor {
841    // Rust constructor for tests and direct Rust usage
842    pub fn new(config: Option<DataActorConfig>) -> Self {
843        let config = config.unwrap_or_default();
844        let core = DataActorCore::new(config);
845        let clock = PyClock::new_test(); // Temporary clock, will be updated on registration
846        let logger = PyLogger::new(core.actor_id().as_str());
847
848        let inner = PyDataActorInner {
849            core,
850            py_self: None,
851            config: None,
852            clock,
853            logger,
854        };
855
856        Self {
857            inner: Rc::new(UnsafeCell::new(inner)),
858        }
859    }
860
861    /// Creates a new [`PyDataActor`] from a Python config object.
862    ///
863    /// Extracts a [`DataActorConfig`] when the object provides one, falling back to defaults
864    /// otherwise, and retains the original object so `.config` returns what the caller passed.
865    /// Subclass constructors share this so the base is built the same way everywhere.
866    #[must_use]
867    pub fn from_py_config(config: Option<Py<PyAny>>) -> Self {
868        let actor_config = config
869            .as_ref()
870            .and_then(|obj| Python::attach(|py| obj.extract::<DataActorConfig>(py).ok()));
871        let mut actor = Self::new(actor_config);
872        actor.set_config(config);
873        actor
874    }
875
876    /// Sets the Python instance reference for method dispatch.
877    ///
878    /// This enables the `PyDataActor` to forward method calls (like `on_start`, `on_stop`)
879    /// to the original Python instance that contains this `PyDataActor`. This is essential
880    /// for Python inheritance to work correctly, allowing Python subclasses to override
881    /// `DataActor` methods and have them called by the Rust system.
882    ///
883    /// Only a weak reference is stored, so the caller keeps ownership of `py_obj`. The trader
884    /// owns registered wrappers; an unregistered actor stays collectable.
885    ///
886    /// # Errors
887    ///
888    /// Returns an error if `py_obj` cannot be weakly referenced.
889    pub fn set_python_instance(&mut self, py_obj: &Bound<'_, PyAny>) -> PyResult<()> {
890        self.inner_mut().py_self = Some(PyWeakrefReference::new(py_obj)?.unbind());
891        Ok(())
892    }
893
894    /// Stores the original Python config object passed at construction.
895    ///
896    /// Retained so the constructed instance exposes `.config` and instance-based registration can
897    /// source the actor ID and logging flags from the same single config object.
898    pub fn set_config(&mut self, config: Option<Py<PyAny>>) {
899        self.inner_mut().config = config;
900    }
901
902    /// Updates the `actor_id` in both the core config and the `actor_id` field.
903    ///
904    /// This method is only exposed for the Python actor to assist with configuration and should
905    /// **never** be called post registration. Calling this after registration will cause
906    /// inconsistent state where the actor is registered under one ID but its internal `actor_id`
907    /// field contains another, breaking message routing and lifecycle management.
908    pub fn set_actor_id(&mut self, actor_id: ActorId) {
909        let inner = self.inner_mut();
910        inner.core.config.actor_id = Some(actor_id);
911        inner.core.actor_id = actor_id;
912        inner.logger = PyLogger::new(actor_id.as_str());
913    }
914
915    /// Updates the `log_events` setting in the core config.
916    pub fn set_log_events(&mut self, log_events: bool) {
917        self.inner_mut().core.config.log_events = log_events;
918    }
919
920    /// Updates the `log_commands` setting in the core config.
921    pub fn set_log_commands(&mut self, log_commands: bool) {
922        self.inner_mut().core.config.log_commands = log_commands;
923    }
924
925    /// Returns the memory address of this instance as a hexadecimal string.
926    pub fn mem_address(&self) -> String {
927        self.inner().core.mem_address()
928    }
929
930    /// Returns a value indicating whether the actor has been registered with a trader.
931    pub fn is_registered(&self) -> bool {
932        self.inner().core.is_registered()
933    }
934
935    /// Register the actor with a trader.
936    ///
937    /// # Errors
938    ///
939    /// Returns an error if the actor is already registered or if the registration process fails.
940    pub fn register(
941        &mut self,
942        trader_id: TraderId,
943        clock: Rc<RefCell<dyn Clock>>,
944        cache: Rc<RefCell<Cache>>,
945    ) -> anyhow::Result<()> {
946        let inner = self.inner_mut();
947        inner.core.register(trader_id, clock, cache)?;
948
949        inner.clock = PyClock::from_rc(inner.core.clock_rc());
950
951        // Register default time event handler for this actor
952        let actor_id = inner.actor_id().inner();
953        let callback = TimeEventCallback::from(move |event: TimeEvent| {
954            if let Some(mut actor) = try_get_actor_unchecked::<PyDataActorInner>(&actor_id) {
955                if let Err(e) = actor.on_time_event(&event) {
956                    log::error!("Python time event handler failed for actor {actor_id}: {e}");
957                }
958            } else {
959                log::error!("Actor {actor_id} not found for time event handling");
960            }
961        });
962
963        inner.clock.inner_mut().register_default_handler(callback);
964
965        inner.initialize()
966    }
967
968    /// Registers this actor in the global component, actor, and wrapper registries.
969    ///
970    /// Clones the internal `Rc` and inserts into both registries. This ensures
971    /// Python and the registries share the exact same actor instance. The Python wrapper is
972    /// retained as part of the same act, so a registered actor always has an owner for the
973    /// wrapper its inner only weakly references.
974    ///
975    /// # Errors
976    ///
977    /// Returns an error if no Python wrapper is attached, or if the attached wrapper has already
978    /// been collected. Nothing is registered in that case.
979    pub fn register_in_global_registries(&self) -> PyResult<()> {
980        let inner = self.inner();
981        let component_id = inner.component_id();
982        let actor_id = Actor::id(inner);
983
984        let Some(wrapper) = inner.python_instance()? else {
985            return Err(to_pyruntime_err(format!(
986                "Cannot register actor {actor_id} without a Python wrapper, call `set_python_instance` first"
987            )));
988        };
989
990        let inner_ref: Rc<UnsafeCell<PyDataActorInner>> = self.inner.clone();
991
992        let component_trait_ref: Rc<UnsafeCell<dyn Component>> = inner_ref.clone();
993        with_component_registry(|registry| {
994            registry.insert(component_id.inner(), component_trait_ref);
995        });
996
997        let actor_trait_ref: Rc<UnsafeCell<dyn Actor>> = inner_ref;
998        with_actor_registry(|registry| registry.insert(actor_id, actor_trait_ref));
999
1000        retain_python_wrapper(component_id, wrapper, inner.core.message_bus());
1001
1002        Ok(())
1003    }
1004}
1005
1006pub fn register_python_exec_algorithm_endpoint(exec_algorithm_id: ExecAlgorithmId) {
1007    let actor_id = exec_algorithm_id.inner();
1008    let endpoint: Ustr = format!("{exec_algorithm_id}.execute").into();
1009    let handler = ShareableMessageHandler::from_typed(move |command: &TradingCommand| {
1010        if let Some(mut algo) = try_get_actor_unchecked::<PyDataActorInner>(&actor_id) {
1011            if let Err(e) = algo.execute_exec_algorithm_command(command) {
1012                log::error!("Error executing command on Python algorithm {actor_id}: {e}");
1013            }
1014        } else {
1015            log::error!("Python execution algorithm {actor_id} not found in registry");
1016        }
1017    });
1018    msgbus::register_any(endpoint.into(), handler);
1019}
1020
1021impl DataActor for PyDataActorInner {
1022    fn on_start(&mut self) -> anyhow::Result<()> {
1023        self.dispatch_on_start()
1024            .map_err(|e| anyhow::anyhow!("Python on_start failed:\n{}", format_exception(&e)))
1025    }
1026
1027    fn on_stop(&mut self) -> anyhow::Result<()> {
1028        self.dispatch_on_stop()
1029            .map_err(|e| anyhow::anyhow!("Python on_stop failed:\n{}", format_exception(&e)))
1030    }
1031
1032    fn on_resume(&mut self) -> anyhow::Result<()> {
1033        self.dispatch_on_resume()
1034            .map_err(|e| anyhow::anyhow!("Python on_resume failed:\n{}", format_exception(&e)))
1035    }
1036
1037    fn on_reset(&mut self) -> anyhow::Result<()> {
1038        self.dispatch_on_reset()
1039            .map_err(|e| anyhow::anyhow!("Python on_reset failed:\n{}", format_exception(&e)))
1040    }
1041
1042    fn on_dispose(&mut self) -> anyhow::Result<()> {
1043        self.dispatch_on_dispose()
1044            .map_err(|e| anyhow::anyhow!("Python on_dispose failed:\n{}", format_exception(&e)))
1045    }
1046
1047    fn on_degrade(&mut self) -> anyhow::Result<()> {
1048        self.dispatch_on_degrade()
1049            .map_err(|e| anyhow::anyhow!("Python on_degrade failed:\n{}", format_exception(&e)))
1050    }
1051
1052    fn on_fault(&mut self) -> anyhow::Result<()> {
1053        self.dispatch_on_fault()
1054            .map_err(|e| anyhow::anyhow!("Python on_fault failed:\n{}", format_exception(&e)))
1055    }
1056
1057    fn on_save(&self) -> anyhow::Result<IndexMap<String, Vec<u8>>> {
1058        self.dispatch_on_save()
1059            .map_err(|e| anyhow::anyhow!("Python on_save failed:\n{}", format_exception(&e)))
1060    }
1061
1062    fn on_load(&mut self, state: IndexMap<String, Vec<u8>>) -> anyhow::Result<()> {
1063        self.dispatch_on_load(&state)
1064            .map_err(|e| anyhow::anyhow!("Python on_load failed:\n{}", format_exception(&e)))
1065    }
1066
1067    fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()> {
1068        self.dispatch_on_time_event(event.clone())
1069            .map_err(|e| anyhow::anyhow!("Python on_time_event failed:\n{}", format_exception(&e)))
1070    }
1071
1072    #[allow(unused_variables)]
1073    fn on_data(&mut self, data: &CustomData) -> anyhow::Result<()> {
1074        Python::attach(|py| {
1075            let py_data: Py<PyAny> = Py::new(py, data.clone())?.into_any();
1076            self.dispatch_on_data(py_data)
1077                .map_err(|e| anyhow::anyhow!("Python on_data failed:\n{}", format_exception(&e)))
1078        })
1079    }
1080
1081    fn on_signal(&mut self, signal: &Signal) -> anyhow::Result<()> {
1082        self.dispatch_on_signal(signal)
1083            .map_err(|e| anyhow::anyhow!("Python on_signal failed:\n{}", format_exception(&e)))
1084    }
1085
1086    fn on_queue_state(&mut self, event: &QueueStateChanged) -> anyhow::Result<()> {
1087        self.dispatch_on_queue_state(event)
1088            .map_err(|e| anyhow::anyhow!("Python on_queue_state failed:\n{}", format_exception(&e)))
1089    }
1090
1091    fn on_socket_state(&mut self, event: &SocketStateChanged) -> anyhow::Result<()> {
1092        self.dispatch_on_socket_state(event).map_err(|e| {
1093            anyhow::anyhow!("Python on_socket_state failed:\n{}", format_exception(&e))
1094        })
1095    }
1096
1097    fn on_instrument(&mut self, instrument: &InstrumentAny) -> anyhow::Result<()> {
1098        Python::attach(|py| {
1099            let py_instrument = instrument_any_to_pyobject(py, instrument.clone())
1100                .map_err(|e| anyhow::anyhow!("Failed to convert InstrumentAny to Python: {e}"))?;
1101            self.dispatch_on_instrument(py_instrument).map_err(|e| {
1102                anyhow::anyhow!("Python on_instrument failed:\n{}", format_exception(&e))
1103            })
1104        })
1105    }
1106
1107    fn on_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
1108        self.dispatch_on_quote(*quote)
1109            .map_err(|e| anyhow::anyhow!("Python on_quote failed:\n{}", format_exception(&e)))
1110    }
1111
1112    fn on_trade(&mut self, tick: &TradeTick) -> anyhow::Result<()> {
1113        self.dispatch_on_trade(*tick)
1114            .map_err(|e| anyhow::anyhow!("Python on_trade failed:\n{}", format_exception(&e)))
1115    }
1116
1117    fn on_bar(&mut self, bar: &Bar) -> anyhow::Result<()> {
1118        self.dispatch_on_bar(*bar)
1119            .map_err(|e| anyhow::anyhow!("Python on_bar failed:\n{}", format_exception(&e)))
1120    }
1121
1122    fn on_book_deltas(&mut self, deltas: &OrderBookDeltas) -> anyhow::Result<()> {
1123        self.dispatch_on_book_deltas(deltas.clone())
1124            .map_err(|e| anyhow::anyhow!("Python on_book_deltas failed:\n{}", format_exception(&e)))
1125    }
1126
1127    fn on_book_depth(&mut self, depth: &OrderBookDepth) -> anyhow::Result<()> {
1128        self.dispatch_on_book_depth(depth)
1129            .map_err(|e| anyhow::anyhow!("Python on_book_depth failed:\n{}", format_exception(&e)))
1130    }
1131
1132    fn on_book(&mut self, order_book: &OrderBook) -> anyhow::Result<()> {
1133        self.dispatch_on_book(order_book)
1134            .map_err(|e| anyhow::anyhow!("Python on_book failed:\n{}", format_exception(&e)))
1135    }
1136
1137    fn on_mark_price(&mut self, mark_price: &MarkPriceUpdate) -> anyhow::Result<()> {
1138        self.dispatch_on_mark_price(*mark_price)
1139            .map_err(|e| anyhow::anyhow!("Python on_mark_price failed:\n{}", format_exception(&e)))
1140    }
1141
1142    fn on_index_price(&mut self, index_price: &IndexPriceUpdate) -> anyhow::Result<()> {
1143        self.dispatch_on_index_price(*index_price)
1144            .map_err(|e| anyhow::anyhow!("Python on_index_price failed:\n{}", format_exception(&e)))
1145    }
1146
1147    fn on_funding_rate(&mut self, funding_rate: &FundingRateUpdate) -> anyhow::Result<()> {
1148        self.dispatch_on_funding_rate(*funding_rate).map_err(|e| {
1149            anyhow::anyhow!("Python on_funding_rate failed:\n{}", format_exception(&e))
1150        })
1151    }
1152
1153    fn on_instrument_status(&mut self, data: &InstrumentStatus) -> anyhow::Result<()> {
1154        self.dispatch_on_instrument_status(*data).map_err(|e| {
1155            anyhow::anyhow!(
1156                "Python on_instrument_status failed:\n{}",
1157                format_exception(&e)
1158            )
1159        })
1160    }
1161
1162    fn on_instrument_close(&mut self, update: &InstrumentClose) -> anyhow::Result<()> {
1163        self.dispatch_on_instrument_close(*update).map_err(|e| {
1164            anyhow::anyhow!(
1165                "Python on_instrument_close failed:\n{}",
1166                format_exception(&e)
1167            )
1168        })
1169    }
1170
1171    fn on_option_greeks(&mut self, greeks: &OptionGreeks) -> anyhow::Result<()> {
1172        self.dispatch_on_option_greeks(*greeks).map_err(|e| {
1173            anyhow::anyhow!("Python on_option_greeks failed:\n{}", format_exception(&e))
1174        })
1175    }
1176
1177    fn on_option_chain(&mut self, slice: &OptionChainSlice) -> anyhow::Result<()> {
1178        self.dispatch_on_option_chain(slice.clone()).map_err(|e| {
1179            anyhow::anyhow!("Python on_option_chain failed:\n{}", format_exception(&e))
1180        })
1181    }
1182
1183    #[cfg(feature = "defi")]
1184    fn on_block(&mut self, block: &Block) -> anyhow::Result<()> {
1185        self.dispatch_on_block(block.clone())
1186            .map_err(|e| anyhow::anyhow!("Python on_block failed:\n{}", format_exception(&e)))
1187    }
1188
1189    #[cfg(feature = "defi")]
1190    fn on_pool(&mut self, pool: &Pool) -> anyhow::Result<()> {
1191        self.dispatch_on_pool(pool.clone())
1192            .map_err(|e| anyhow::anyhow!("Python on_pool failed:\n{}", format_exception(&e)))
1193    }
1194
1195    #[cfg(feature = "defi")]
1196    fn on_pool_swap(&mut self, swap: &PoolSwap) -> anyhow::Result<()> {
1197        self.dispatch_on_pool_swap(swap.clone())
1198            .map_err(|e| anyhow::anyhow!("Python on_pool_swap failed:\n{}", format_exception(&e)))
1199    }
1200
1201    #[cfg(feature = "defi")]
1202    fn on_pool_liquidity_update(&mut self, update: &PoolLiquidityUpdate) -> anyhow::Result<()> {
1203        self.dispatch_on_pool_liquidity_update(update.clone())
1204            .map_err(|e| {
1205                anyhow::anyhow!(
1206                    "Python on_pool_liquidity_update failed:\n{}",
1207                    format_exception(&e)
1208                )
1209            })
1210    }
1211
1212    #[cfg(feature = "defi")]
1213    fn on_pool_fee_collect(&mut self, collect: &PoolFeeCollect) -> anyhow::Result<()> {
1214        self.dispatch_on_pool_fee_collect(collect.clone())
1215            .map_err(|e| {
1216                anyhow::anyhow!(
1217                    "Python on_pool_fee_collect failed:\n{}",
1218                    format_exception(&e)
1219                )
1220            })
1221    }
1222
1223    #[cfg(feature = "defi")]
1224    fn on_pool_flash(&mut self, flash: &PoolFlash) -> anyhow::Result<()> {
1225        self.dispatch_on_pool_flash(flash.clone())
1226            .map_err(|e| anyhow::anyhow!("Python on_pool_flash failed:\n{}", format_exception(&e)))
1227    }
1228
1229    fn on_historical_data(&mut self, data: &dyn Any) -> anyhow::Result<()> {
1230        Python::attach(|py| {
1231            let py_data: Py<PyAny> = if let Some(custom_data) = data.downcast_ref::<CustomData>() {
1232                Py::new(py, custom_data.clone())?.into_any()
1233            } else if let Some(custom_data) = data.downcast_ref::<Vec<CustomData>>() {
1234                custom_data.clone().into_py_any(py)?
1235            } else {
1236                anyhow::bail!("Failed to convert historical data to Python: unsupported type");
1237            };
1238
1239            self.dispatch_on_historical_data(py_data).map_err(|e| {
1240                anyhow::anyhow!(
1241                    "Python on_historical_data failed:\n{}",
1242                    format_exception(&e)
1243                )
1244            })
1245        })
1246    }
1247
1248    fn on_historical_book_deltas(&mut self, deltas: &[OrderBookDelta]) -> anyhow::Result<()> {
1249        self.dispatch_on_historical_book_deltas(deltas.to_vec())
1250            .map_err(|e| {
1251                anyhow::anyhow!(
1252                    "Python on_historical_book_deltas failed:\n{}",
1253                    format_exception(&e)
1254                )
1255            })
1256    }
1257
1258    fn on_historical_book_depth(&mut self, depths: &[OrderBookDepth]) -> anyhow::Result<()> {
1259        self.dispatch_on_historical_book_depth(depths.to_vec())
1260            .map_err(|e| {
1261                anyhow::anyhow!(
1262                    "Python on_historical_book_depth failed:\n{}",
1263                    format_exception(&e)
1264                )
1265            })
1266    }
1267
1268    fn on_historical_quotes(&mut self, quotes: &[QuoteTick]) -> anyhow::Result<()> {
1269        self.dispatch_on_historical_quotes(quotes.to_vec())
1270            .map_err(|e| {
1271                anyhow::anyhow!(
1272                    "Python on_historical_quotes failed:\n{}",
1273                    format_exception(&e)
1274                )
1275            })
1276    }
1277
1278    fn on_historical_trades(&mut self, trades: &[TradeTick]) -> anyhow::Result<()> {
1279        self.dispatch_on_historical_trades(trades.to_vec())
1280            .map_err(|e| {
1281                anyhow::anyhow!(
1282                    "Python on_historical_trades failed:\n{}",
1283                    format_exception(&e)
1284                )
1285            })
1286    }
1287
1288    fn on_historical_funding_rates(
1289        &mut self,
1290        funding_rates: &[FundingRateUpdate],
1291    ) -> anyhow::Result<()> {
1292        self.dispatch_on_historical_funding_rates(funding_rates.to_vec())
1293            .map_err(|e| {
1294                anyhow::anyhow!(
1295                    "Python on_historical_funding_rates failed:\n{}",
1296                    format_exception(&e)
1297                )
1298            })
1299    }
1300
1301    fn on_historical_bars(&mut self, bars: &[Bar]) -> anyhow::Result<()> {
1302        self.dispatch_on_historical_bars(bars.to_vec())
1303            .map_err(|e| {
1304                anyhow::anyhow!(
1305                    "Python on_historical_bars failed:\n{}",
1306                    format_exception(&e)
1307                )
1308            })
1309    }
1310
1311    fn on_historical_mark_prices(&mut self, mark_prices: &[MarkPriceUpdate]) -> anyhow::Result<()> {
1312        self.dispatch_on_historical_mark_prices(mark_prices.to_vec())
1313            .map_err(|e| {
1314                anyhow::anyhow!(
1315                    "Python on_historical_mark_prices failed:\n{}",
1316                    format_exception(&e)
1317                )
1318            })
1319    }
1320
1321    fn on_historical_index_prices(
1322        &mut self,
1323        index_prices: &[IndexPriceUpdate],
1324    ) -> anyhow::Result<()> {
1325        self.dispatch_on_historical_index_prices(index_prices.to_vec())
1326            .map_err(|e| {
1327                anyhow::anyhow!(
1328                    "Python on_historical_index_prices failed:\n{}",
1329                    format_exception(&e)
1330                )
1331            })
1332    }
1333}
1334
1335#[pymethods]
1336#[pyo3_stub_gen::derive::gen_stub_pymethods]
1337impl PyDataActor {
1338    /// Creates a new [`PyDataActor`] instance.
1339    ///
1340    /// Accepts `None` or any Python object. If the object is a [`DataActorConfig`]
1341    /// (or can be extracted as one via `from_py_object`), its values are used;
1342    /// otherwise the actor falls back to [`DataActorConfig::default()`].
1343    ///
1344    /// This permissive signature is required so that Python subclasses can pass a
1345    /// **custom** config dataclass to their `__init__`. The original object is retained
1346    /// here in `__new__`, which always receives the constructor arguments, so `.config`
1347    /// and registration see the config even when a subclass omits forwarding it to
1348    /// `super().__init__()`.
1349    #[new]
1350    #[pyo3(signature = (config=None))]
1351    fn py_new(config: Option<Py<PyAny>>) -> Self {
1352        Self::from_py_config(config)
1353    }
1354
1355    #[pyo3(signature = (config=None))]
1356    fn __init__(slf: &Bound<'_, Self>, config: Option<Py<PyAny>>) -> PyResult<()> {
1357        {
1358            let mut borrowed = slf.borrow_mut();
1359            borrowed.set_python_instance(slf.as_any())?;
1360            // `__new__` retained the config; only a forwarded config overrides it
1361            if config.is_some() {
1362                borrowed.set_config(config);
1363            }
1364        }
1365
1366        if !has_configured_actor_id(slf) {
1367            let py_type = slf.get_type();
1368            let type_name = py_type.name()?;
1369            let actor_id = ActorId::new_checked(type_name.to_str()?).map_err(to_pyvalue_err)?;
1370            slf.borrow_mut().set_actor_id(actor_id);
1371        }
1372
1373        Ok(())
1374    }
1375
1376    #[getter]
1377    #[pyo3(name = "clock")]
1378    fn py_clock(&self) -> PyResult<PyClock> {
1379        let inner = self.inner();
1380        if inner.core.is_registered() {
1381            Ok(inner.clock.clone())
1382        } else {
1383            Err(to_pyruntime_err(
1384                "Actor must be registered with a trader before accessing clock",
1385            ))
1386        }
1387    }
1388
1389    #[getter]
1390    #[pyo3(name = "cache")]
1391    fn py_cache(&self) -> PyResult<PyCache> {
1392        let inner = self.inner();
1393        if inner.core.is_registered() {
1394            Ok(PyCache::from_rc(inner.core.cache_rc()))
1395        } else {
1396            Err(to_pyruntime_err(
1397                "Actor must be registered with a trader before accessing cache",
1398            ))
1399        }
1400    }
1401
1402    #[getter]
1403    #[pyo3(name = "log")]
1404    fn py_log(&self) -> PyLogger {
1405        self.inner().logger.clone()
1406    }
1407
1408    #[getter]
1409    #[pyo3(name = "actor_id")]
1410    fn py_actor_id(&self) -> ActorId {
1411        self.inner().core.actor_id
1412    }
1413
1414    #[getter]
1415    #[pyo3(name = "config")]
1416    fn py_config(&self, py: Python<'_>) -> Option<Py<PyAny>> {
1417        self.inner()
1418            .config
1419            .as_ref()
1420            .map(|config| config.clone_ref(py))
1421    }
1422
1423    #[getter]
1424    #[pyo3(name = "trader_id")]
1425    fn py_trader_id(&self) -> Option<TraderId> {
1426        self.inner().core.trader_id()
1427    }
1428
1429    #[pyo3(name = "state")]
1430    fn py_state(&self) -> ComponentState {
1431        Component::state(self.inner())
1432    }
1433
1434    #[pyo3(name = "is_ready")]
1435    fn py_is_ready(&self) -> bool {
1436        Component::is_ready(self.inner())
1437    }
1438
1439    #[pyo3(name = "is_running")]
1440    fn py_is_running(&self) -> bool {
1441        Component::is_running(self.inner())
1442    }
1443
1444    #[pyo3(name = "is_stopped")]
1445    fn py_is_stopped(&self) -> bool {
1446        Component::is_stopped(self.inner())
1447    }
1448
1449    #[pyo3(name = "is_degraded")]
1450    fn py_is_degraded(&self) -> bool {
1451        Component::is_degraded(self.inner())
1452    }
1453
1454    #[pyo3(name = "is_faulted")]
1455    fn py_is_faulted(&self) -> bool {
1456        Component::is_faulted(self.inner())
1457    }
1458
1459    #[pyo3(name = "is_disposed")]
1460    fn py_is_disposed(&self) -> bool {
1461        Component::is_disposed(self.inner())
1462    }
1463
1464    #[pyo3(name = "start")]
1465    fn py_start(&mut self) -> PyResult<()> {
1466        Component::start(self.inner_mut()).map_err(to_pyruntime_err)
1467    }
1468
1469    #[pyo3(name = "stop")]
1470    fn py_stop(&mut self) -> PyResult<()> {
1471        Component::stop(self.inner_mut()).map_err(to_pyruntime_err)
1472    }
1473
1474    #[pyo3(name = "save")]
1475    fn py_save(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
1476        let state = DataActor::on_save(self.inner()).map_err(to_pyruntime_err)?;
1477        state_to_pydict(py, &state)
1478    }
1479
1480    #[pyo3(name = "load")]
1481    fn py_load(&mut self, state: &Bound<'_, PyDict>) -> PyResult<()> {
1482        let state = pydict_to_state(state)?;
1483        DataActor::on_load(self.inner_mut(), state).map_err(to_pyruntime_err)
1484    }
1485
1486    #[pyo3(name = "resume")]
1487    fn py_resume(&mut self) -> PyResult<()> {
1488        Component::resume(self.inner_mut()).map_err(to_pyruntime_err)
1489    }
1490
1491    #[pyo3(name = "reset")]
1492    fn py_reset(&mut self) -> PyResult<()> {
1493        Component::reset(self.inner_mut()).map_err(to_pyruntime_err)
1494    }
1495
1496    #[pyo3(name = "dispose")]
1497    fn py_dispose(&mut self) -> PyResult<()> {
1498        Component::dispose(self.inner_mut()).map_err(to_pyruntime_err)
1499    }
1500
1501    #[pyo3(name = "degrade")]
1502    fn py_degrade(&mut self) -> PyResult<()> {
1503        Component::degrade(self.inner_mut()).map_err(to_pyruntime_err)
1504    }
1505
1506    #[pyo3(name = "fault")]
1507    fn py_fault(&mut self) -> PyResult<()> {
1508        Component::fault(self.inner_mut()).map_err(to_pyruntime_err)
1509    }
1510
1511    #[pyo3(name = "shutdown_system")]
1512    #[pyo3(signature = (reason=None))]
1513    fn py_shutdown_system(&self, reason: Option<String>) -> PyResult<()> {
1514        if !self.inner().core.is_registered() {
1515            return Err(to_pyruntime_err(
1516                "Actor must be registered with a trader before shutting down the system",
1517            ));
1518        }
1519
1520        self.inner().shutdown_system(reason);
1521        Ok(())
1522    }
1523
1524    #[pyo3(name = "publish_data")]
1525    fn py_publish_data(&self, data_type: &DataType, data: &CustomData) -> PyResult<()> {
1526        self.ensure_registered_for_data()?;
1527        self.inner().publish_data(data_type, data);
1528        Ok(())
1529    }
1530
1531    #[pyo3(name = "publish_signal")]
1532    #[pyo3(signature = (name, value, ts_event=0))]
1533    #[expect(
1534        clippy::needless_pass_by_value,
1535        reason = "PyO3 accepts an owned PyAny handle for Python signal values"
1536    )]
1537    fn py_publish_signal(
1538        &self,
1539        py: Python<'_>,
1540        name: &str,
1541        value: Py<PyAny>,
1542        ts_event: u64,
1543    ) -> PyResult<()> {
1544        self.ensure_registered_for_data()?;
1545        let value_str: String = value.bind(py).str()?.extract()?;
1546        self.inner()
1547            .publish_signal(name, value_str, UnixNanos::from(ts_event));
1548        Ok(())
1549    }
1550
1551    #[pyo3(name = "add_synthetic")]
1552    fn py_add_synthetic(&self, synthetic: SyntheticInstrument) -> PyResult<()> {
1553        self.ensure_registered_for_data()?;
1554        self.inner()
1555            .add_synthetic(synthetic)
1556            .map_err(to_pyvalue_err)
1557    }
1558
1559    #[pyo3(name = "update_synthetic")]
1560    fn py_update_synthetic(&self, synthetic: SyntheticInstrument) -> PyResult<()> {
1561        self.ensure_registered_for_data()?;
1562        self.inner()
1563            .update_synthetic(synthetic)
1564            .map_err(to_pyvalue_err)
1565    }
1566
1567    #[getter]
1568    #[pyo3(name = "registered_indicators")]
1569    fn py_registered_indicators(&self, py: Python<'_>) -> PyResult<Py<PyList>> {
1570        registered_python_indicators(py, self.inner().core.registered_indicators())
1571    }
1572
1573    #[pyo3(name = "indicators_initialized")]
1574    fn py_indicators_initialized(&self, _py: Python<'_>) -> PyResult<bool> {
1575        self.inner()
1576            .core
1577            .indicators_initialized()
1578            .map_err(to_pyruntime_err)
1579    }
1580
1581    #[pyo3(name = "register_indicator_for_quote_ticks")]
1582    fn py_register_indicator_for_quote_ticks(
1583        &mut self,
1584        py: Python<'_>,
1585        instrument_id: InstrumentId,
1586        indicator: Py<PyAny>,
1587    ) {
1588        let indicator = wrap_python_indicator(py, indicator);
1589        self.inner_mut()
1590            .core
1591            .register_indicator_for_quote_ticks(instrument_id, indicator);
1592    }
1593
1594    #[pyo3(name = "register_indicator_for_trade_ticks")]
1595    fn py_register_indicator_for_trade_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_trade_ticks(instrument_id, indicator);
1605    }
1606
1607    #[pyo3(name = "register_indicator_for_bars")]
1608    fn py_register_indicator_for_bars(
1609        &mut self,
1610        py: Python<'_>,
1611        bar_type: BarType,
1612        indicator: Py<PyAny>,
1613    ) {
1614        let indicator = wrap_python_indicator(py, indicator);
1615        self.inner_mut()
1616            .core
1617            .register_indicator_for_bars(bar_type, indicator);
1618    }
1619
1620    #[pyo3(name = "on_start")]
1621    fn py_on_start(_slf: &Bound<'_, Self>) {}
1622
1623    #[pyo3(name = "on_stop")]
1624    fn py_on_stop(_slf: &Bound<'_, Self>) {}
1625
1626    #[pyo3(name = "on_resume")]
1627    fn py_on_resume(_slf: &Bound<'_, Self>) {}
1628
1629    #[pyo3(name = "on_reset")]
1630    fn py_on_reset(_slf: &Bound<'_, Self>) {}
1631
1632    #[pyo3(name = "on_dispose")]
1633    fn py_on_dispose(_slf: &Bound<'_, Self>) {}
1634
1635    #[pyo3(name = "on_degrade")]
1636    fn py_on_degrade(_slf: &Bound<'_, Self>) {}
1637
1638    #[pyo3(name = "on_fault")]
1639    fn py_on_fault(_slf: &Bound<'_, Self>) {}
1640
1641    #[pyo3(name = "on_save")]
1642    fn py_on_save(_slf: &Bound<'_, Self>, py: Python<'_>) -> Py<PyDict> {
1643        PyDict::new(py).unbind()
1644    }
1645
1646    #[allow(unused_variables)]
1647    #[pyo3(name = "on_load")]
1648    fn py_on_load(_slf: &Bound<'_, Self>, state: &Bound<'_, PyDict>) {}
1649
1650    #[allow(unused_variables, clippy::needless_pass_by_value)]
1651    #[pyo3(name = "on_time_event")]
1652    fn py_on_time_event(_slf: &Bound<'_, Self>, event: TimeEvent) {}
1653
1654    #[allow(unused_variables, clippy::needless_pass_by_value)]
1655    #[pyo3(name = "on_data")]
1656    fn py_on_data(_slf: &Bound<'_, Self>, data: Py<PyAny>) {}
1657
1658    #[allow(unused_variables)]
1659    #[pyo3(name = "on_signal")]
1660    fn py_on_signal(_slf: &Bound<'_, Self>, signal: &Signal) {}
1661
1662    #[allow(unused_variables, clippy::needless_pass_by_value)]
1663    #[pyo3(name = "on_queue_state")]
1664    fn py_on_queue_state(_slf: &Bound<'_, Self>, event: QueueStateChanged) {}
1665
1666    #[allow(unused_variables, clippy::needless_pass_by_value)]
1667    #[pyo3(name = "on_socket_state")]
1668    fn py_on_socket_state(_slf: &Bound<'_, Self>, event: SocketStateChanged) {}
1669
1670    #[allow(unused_variables, clippy::needless_pass_by_value)]
1671    #[pyo3(name = "on_instrument")]
1672    fn py_on_instrument(_slf: &Bound<'_, Self>, instrument: Py<PyAny>) {}
1673
1674    #[allow(unused_variables)]
1675    #[pyo3(name = "on_quote")]
1676    fn py_on_quote(_slf: &Bound<'_, Self>, quote: QuoteTick) {}
1677
1678    #[allow(unused_variables)]
1679    #[pyo3(name = "on_trade")]
1680    fn py_on_trade(_slf: &Bound<'_, Self>, trade: TradeTick) {}
1681
1682    #[allow(unused_variables)]
1683    #[pyo3(name = "on_bar")]
1684    fn py_on_bar(_slf: &Bound<'_, Self>, bar: Bar) {}
1685
1686    #[allow(unused_variables, clippy::needless_pass_by_value)]
1687    #[pyo3(name = "on_book_deltas")]
1688    fn py_on_book_deltas(_slf: &Bound<'_, Self>, deltas: OrderBookDeltas) {}
1689
1690    #[allow(unused_variables)]
1691    #[pyo3(name = "on_book_depth")]
1692    fn py_on_book_depth(_slf: &Bound<'_, Self>, depth: &OrderBookDepth) {}
1693
1694    #[allow(unused_variables)]
1695    #[pyo3(name = "on_book")]
1696    fn py_on_book(_slf: &Bound<'_, Self>, book: &OrderBook) {}
1697
1698    #[allow(unused_variables)]
1699    #[pyo3(name = "on_mark_price")]
1700    fn py_on_mark_price(_slf: &Bound<'_, Self>, mark_price: MarkPriceUpdate) {}
1701
1702    #[allow(unused_variables)]
1703    #[pyo3(name = "on_index_price")]
1704    fn py_on_index_price(_slf: &Bound<'_, Self>, index_price: IndexPriceUpdate) {}
1705
1706    #[allow(unused_variables)]
1707    #[pyo3(name = "on_funding_rate")]
1708    fn py_on_funding_rate(_slf: &Bound<'_, Self>, funding_rate: FundingRateUpdate) {}
1709
1710    #[allow(unused_variables)]
1711    #[pyo3(name = "on_instrument_status")]
1712    fn py_on_instrument_status(_slf: &Bound<'_, Self>, status: InstrumentStatus) {}
1713
1714    #[allow(unused_variables)]
1715    #[pyo3(name = "on_instrument_close")]
1716    fn py_on_instrument_close(_slf: &Bound<'_, Self>, close: InstrumentClose) {}
1717
1718    #[allow(unused_variables)]
1719    #[pyo3(name = "on_option_greeks")]
1720    fn py_on_option_greeks(_slf: &Bound<'_, Self>, greeks: OptionGreeks) {}
1721
1722    #[allow(unused_variables, clippy::needless_pass_by_value)]
1723    #[pyo3(name = "on_option_chain")]
1724    fn py_on_option_chain(_slf: &Bound<'_, Self>, slice: OptionChainSlice) {}
1725
1726    #[pyo3(name = "subscribe_data")]
1727    #[pyo3(signature = (data_type, client_id=None, params=None))]
1728    fn py_subscribe_data(
1729        &mut self,
1730        py: Python<'_>,
1731        data_type: DataType,
1732        client_id: Option<ClientId>,
1733        params: Option<Py<PyDict>>,
1734    ) -> PyResult<()> {
1735        self.ensure_registered()?;
1736        let params = dict_to_params(py, params)?;
1737        DataActor::subscribe_data(self.inner_mut(), data_type, client_id, params);
1738        Ok(())
1739    }
1740
1741    #[pyo3(name = "subscribe_signal")]
1742    #[pyo3(signature = (name="", priority=None))]
1743    fn py_subscribe_signal(
1744        slf: &Bound<'_, Self>,
1745        name: &str,
1746        priority: Option<u32>,
1747    ) -> PyResult<()> {
1748        let actor = borrow_actor_mut(slf, "subscribe_signal")?;
1749        actor.ensure_registered()?;
1750        DataActor::subscribe_signal(actor.inner_mut(), name, priority);
1751        Ok(())
1752    }
1753
1754    #[pyo3(name = "subscribe_queue_state")]
1755    #[pyo3(signature = (channel=None, priority=None))]
1756    fn py_subscribe_queue_state(
1757        &mut self,
1758        channel: Option<SystemChannel>,
1759        priority: Option<u32>,
1760    ) -> PyResult<()> {
1761        self.ensure_registered()?;
1762        DataActor::subscribe_queue_state(self.inner_mut(), channel, priority);
1763        Ok(())
1764    }
1765
1766    #[pyo3(name = "subscribe_socket_state")]
1767    #[pyo3(signature = (client_id=None, endpoint=None, priority=None))]
1768    fn py_subscribe_socket_state(
1769        &mut self,
1770        client_id: Option<ClientId>,
1771        endpoint: Option<&str>,
1772        priority: Option<u32>,
1773    ) -> PyResult<()> {
1774        self.ensure_registered()?;
1775        DataActor::subscribe_socket_state(self.inner_mut(), client_id, endpoint, priority);
1776        Ok(())
1777    }
1778
1779    #[pyo3(name = "subscribe_instruments")]
1780    #[pyo3(signature = (venue, client_id=None, params=None))]
1781    fn py_subscribe_instruments(
1782        &mut self,
1783        py: Python<'_>,
1784        venue: Venue,
1785        client_id: Option<ClientId>,
1786        params: Option<Py<PyDict>>,
1787    ) -> PyResult<()> {
1788        self.ensure_registered()?;
1789        let params = dict_to_params(py, params)?;
1790        DataActor::subscribe_instruments(self.inner_mut(), venue, client_id, params);
1791        Ok(())
1792    }
1793
1794    #[pyo3(name = "subscribe_instrument")]
1795    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1796    fn py_subscribe_instrument(
1797        &mut self,
1798        py: Python<'_>,
1799        instrument_id: InstrumentId,
1800        client_id: Option<ClientId>,
1801        params: Option<Py<PyDict>>,
1802    ) -> PyResult<()> {
1803        self.ensure_registered()?;
1804        let params = dict_to_params(py, params)?;
1805        DataActor::subscribe_instrument(self.inner_mut(), instrument_id, client_id, params);
1806        Ok(())
1807    }
1808
1809    #[pyo3(name = "subscribe_book_deltas")]
1810    #[pyo3(signature = (instrument_id, book_type, depth=None, client_id=None, managed=false, params=None))]
1811    #[expect(clippy::too_many_arguments)]
1812    fn py_subscribe_book_deltas(
1813        &mut self,
1814        py: Python<'_>,
1815        instrument_id: InstrumentId,
1816        book_type: BookType,
1817        depth: Option<usize>,
1818        client_id: Option<ClientId>,
1819        managed: bool,
1820        params: Option<Py<PyDict>>,
1821    ) -> PyResult<()> {
1822        self.ensure_registered()?;
1823        let params = dict_to_params(py, params)?;
1824        let depth = depth.and_then(NonZeroUsize::new);
1825        DataActor::subscribe_book_deltas(
1826            self.inner_mut(),
1827            instrument_id,
1828            book_type,
1829            depth,
1830            client_id,
1831            managed,
1832            params,
1833        );
1834        Ok(())
1835    }
1836
1837    #[expect(clippy::too_many_arguments)]
1838    #[pyo3(name = "subscribe_book_depth")]
1839    #[pyo3(signature = (instrument_id, book_type, depth=None, client_id=None, managed=false, params=None))]
1840    fn py_subscribe_book_depth(
1841        &mut self,
1842        py: Python<'_>,
1843        instrument_id: InstrumentId,
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
1852        let depth = depth
1853            .map(|value| {
1854                NonZeroUsize::new(value).ok_or_else(|| to_pyvalue_err("depth must be positive"))
1855            })
1856            .transpose()?;
1857
1858        let params = dict_to_params(py, params)?;
1859        DataActor::subscribe_book_depth(
1860            self.inner_mut(),
1861            instrument_id,
1862            book_type,
1863            depth,
1864            client_id,
1865            managed,
1866            params,
1867        );
1868        Ok(())
1869    }
1870
1871    #[pyo3(name = "subscribe_book_at_interval")]
1872    #[pyo3(signature = (instrument_id, book_type, interval_ms, depth=None, client_id=None, params=None))]
1873    #[expect(clippy::too_many_arguments)]
1874    fn py_subscribe_book_at_interval(
1875        &mut self,
1876        py: Python<'_>,
1877        instrument_id: InstrumentId,
1878        book_type: BookType,
1879        interval_ms: usize,
1880        depth: Option<usize>,
1881        client_id: Option<ClientId>,
1882        params: Option<Py<PyDict>>,
1883    ) -> PyResult<()> {
1884        let interval_ms = NonZeroUsize::new(interval_ms)
1885            .ok_or_else(|| to_pyvalue_err("interval_ms must be > 0"))?;
1886
1887        self.ensure_registered()?;
1888        let params = dict_to_params(py, params)?;
1889        let depth = depth.and_then(NonZeroUsize::new);
1890        DataActor::subscribe_book_at_interval(
1891            self.inner_mut(),
1892            instrument_id,
1893            book_type,
1894            depth,
1895            interval_ms,
1896            client_id,
1897            params,
1898        );
1899        Ok(())
1900    }
1901
1902    #[pyo3(name = "subscribe_quotes")]
1903    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1904    fn py_subscribe_quotes(
1905        &mut self,
1906        py: Python<'_>,
1907        instrument_id: InstrumentId,
1908        client_id: Option<ClientId>,
1909        params: Option<Py<PyDict>>,
1910    ) -> PyResult<()> {
1911        self.ensure_registered()?;
1912        let params = dict_to_params(py, params)?;
1913        DataActor::subscribe_quotes(self.inner_mut(), instrument_id, client_id, params);
1914        Ok(())
1915    }
1916
1917    #[pyo3(name = "subscribe_trades")]
1918    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1919    fn py_subscribe_trades(
1920        &mut self,
1921        py: Python<'_>,
1922        instrument_id: InstrumentId,
1923        client_id: Option<ClientId>,
1924        params: Option<Py<PyDict>>,
1925    ) -> PyResult<()> {
1926        self.ensure_registered()?;
1927        let params = dict_to_params(py, params)?;
1928        DataActor::subscribe_trades(self.inner_mut(), instrument_id, client_id, params);
1929        Ok(())
1930    }
1931
1932    #[pyo3(name = "subscribe_bars")]
1933    #[pyo3(signature = (bar_type, client_id=None, params=None))]
1934    fn py_subscribe_bars(
1935        &mut self,
1936        py: Python<'_>,
1937        bar_type: BarType,
1938        client_id: Option<ClientId>,
1939        params: Option<Py<PyDict>>,
1940    ) -> PyResult<()> {
1941        self.ensure_registered()?;
1942        let params = dict_to_params(py, params)?;
1943        DataActor::subscribe_bars(self.inner_mut(), bar_type, client_id, params);
1944        Ok(())
1945    }
1946
1947    #[pyo3(name = "subscribe_mark_prices")]
1948    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1949    fn py_subscribe_mark_prices(
1950        &mut self,
1951        py: Python<'_>,
1952        instrument_id: InstrumentId,
1953        client_id: Option<ClientId>,
1954        params: Option<Py<PyDict>>,
1955    ) -> PyResult<()> {
1956        self.ensure_registered()?;
1957        let params = dict_to_params(py, params)?;
1958        DataActor::subscribe_mark_prices(self.inner_mut(), instrument_id, client_id, params);
1959        Ok(())
1960    }
1961
1962    #[pyo3(name = "subscribe_index_prices")]
1963    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1964    fn py_subscribe_index_prices(
1965        &mut self,
1966        py: Python<'_>,
1967        instrument_id: InstrumentId,
1968        client_id: Option<ClientId>,
1969        params: Option<Py<PyDict>>,
1970    ) -> PyResult<()> {
1971        self.ensure_registered()?;
1972        let params = dict_to_params(py, params)?;
1973        DataActor::subscribe_index_prices(self.inner_mut(), instrument_id, client_id, params);
1974        Ok(())
1975    }
1976
1977    #[pyo3(name = "subscribe_funding_rates")]
1978    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1979    fn py_subscribe_funding_rates(
1980        &mut self,
1981        py: Python<'_>,
1982        instrument_id: InstrumentId,
1983        client_id: Option<ClientId>,
1984        params: Option<Py<PyDict>>,
1985    ) -> PyResult<()> {
1986        self.ensure_registered()?;
1987        let params = dict_to_params(py, params)?;
1988        DataActor::subscribe_funding_rates(self.inner_mut(), instrument_id, client_id, params);
1989        Ok(())
1990    }
1991
1992    #[pyo3(name = "subscribe_option_greeks")]
1993    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1994    fn py_subscribe_option_greeks(
1995        &mut self,
1996        py: Python<'_>,
1997        instrument_id: InstrumentId,
1998        client_id: Option<ClientId>,
1999        params: Option<Py<PyDict>>,
2000    ) -> PyResult<()> {
2001        self.ensure_registered()?;
2002        let params = dict_to_params(py, params)?;
2003        DataActor::subscribe_option_greeks(self.inner_mut(), instrument_id, client_id, params);
2004        Ok(())
2005    }
2006
2007    #[pyo3(name = "subscribe_instrument_status")]
2008    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2009    fn py_subscribe_instrument_status(
2010        &mut self,
2011        py: Python<'_>,
2012        instrument_id: InstrumentId,
2013        client_id: Option<ClientId>,
2014        params: Option<Py<PyDict>>,
2015    ) -> PyResult<()> {
2016        self.ensure_registered()?;
2017        let params = dict_to_params(py, params)?;
2018        DataActor::subscribe_instrument_status(self.inner_mut(), instrument_id, client_id, params);
2019        Ok(())
2020    }
2021
2022    #[pyo3(name = "subscribe_instrument_close")]
2023    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2024    fn py_subscribe_instrument_close(
2025        &mut self,
2026        py: Python<'_>,
2027        instrument_id: InstrumentId,
2028        client_id: Option<ClientId>,
2029        params: Option<Py<PyDict>>,
2030    ) -> PyResult<()> {
2031        self.ensure_registered()?;
2032        let params = dict_to_params(py, params)?;
2033        DataActor::subscribe_instrument_close(self.inner_mut(), instrument_id, client_id, params);
2034        Ok(())
2035    }
2036
2037    #[pyo3(name = "subscribe_option_chain")]
2038    #[pyo3(signature = (series_id, strike_range, snapshot_interval_ms=None, client_id=None, params=None))]
2039    fn py_subscribe_option_chain(
2040        &mut self,
2041        py: Python<'_>,
2042        series_id: OptionSeriesId,
2043        strike_range: PyStrikeRange,
2044        snapshot_interval_ms: Option<u64>,
2045        client_id: Option<ClientId>,
2046        params: Option<Py<PyDict>>,
2047    ) -> PyResult<()> {
2048        self.ensure_registered()?;
2049        let params = dict_to_params(py, params)?;
2050        DataActor::subscribe_option_chain(
2051            self.inner_mut(),
2052            series_id,
2053            strike_range.inner,
2054            snapshot_interval_ms,
2055            client_id,
2056            params,
2057        );
2058        Ok(())
2059    }
2060
2061    #[pyo3(name = "unsubscribe_data")]
2062    #[pyo3(signature = (data_type, client_id=None, params=None))]
2063    fn py_unsubscribe_data(
2064        &mut self,
2065        py: Python<'_>,
2066        data_type: DataType,
2067        client_id: Option<ClientId>,
2068        params: Option<Py<PyDict>>,
2069    ) -> PyResult<()> {
2070        self.ensure_registered()?;
2071        let params = dict_to_params(py, params)?;
2072        DataActor::unsubscribe_data(self.inner_mut(), data_type, client_id, params);
2073        Ok(())
2074    }
2075
2076    #[pyo3(name = "unsubscribe_signal")]
2077    #[pyo3(signature = (name=""))]
2078    fn py_unsubscribe_signal(slf: &Bound<'_, Self>, name: &str) -> PyResult<()> {
2079        let actor = borrow_actor_mut(slf, "unsubscribe_signal")?;
2080        actor.ensure_registered()?;
2081        DataActor::unsubscribe_signal(actor.inner_mut(), name);
2082        Ok(())
2083    }
2084
2085    #[pyo3(name = "unsubscribe_queue_state")]
2086    #[pyo3(signature = (channel=None))]
2087    fn py_unsubscribe_queue_state(&mut self, channel: Option<SystemChannel>) -> PyResult<()> {
2088        self.ensure_registered()?;
2089        DataActor::unsubscribe_queue_state(self.inner_mut(), channel);
2090        Ok(())
2091    }
2092
2093    #[pyo3(name = "unsubscribe_socket_state")]
2094    #[pyo3(signature = (client_id=None, endpoint=None))]
2095    fn py_unsubscribe_socket_state(
2096        &mut self,
2097        client_id: Option<ClientId>,
2098        endpoint: Option<&str>,
2099    ) -> PyResult<()> {
2100        self.ensure_registered()?;
2101        DataActor::unsubscribe_socket_state(self.inner_mut(), client_id, endpoint);
2102        Ok(())
2103    }
2104
2105    #[pyo3(name = "unsubscribe_instruments")]
2106    #[pyo3(signature = (venue, client_id=None, params=None))]
2107    fn py_unsubscribe_instruments(
2108        &mut self,
2109        py: Python<'_>,
2110        venue: Venue,
2111        client_id: Option<ClientId>,
2112        params: Option<Py<PyDict>>,
2113    ) -> PyResult<()> {
2114        self.ensure_registered()?;
2115        let params = dict_to_params(py, params)?;
2116        DataActor::unsubscribe_instruments(self.inner_mut(), venue, client_id, params);
2117        Ok(())
2118    }
2119
2120    #[pyo3(name = "unsubscribe_instrument")]
2121    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2122    fn py_unsubscribe_instrument(
2123        &mut self,
2124        py: Python<'_>,
2125        instrument_id: InstrumentId,
2126        client_id: Option<ClientId>,
2127        params: Option<Py<PyDict>>,
2128    ) -> PyResult<()> {
2129        self.ensure_registered()?;
2130        let params = dict_to_params(py, params)?;
2131        DataActor::unsubscribe_instrument(self.inner_mut(), instrument_id, client_id, params);
2132        Ok(())
2133    }
2134
2135    #[pyo3(name = "unsubscribe_book_deltas")]
2136    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2137    fn py_unsubscribe_book_deltas(
2138        &mut self,
2139        py: Python<'_>,
2140        instrument_id: InstrumentId,
2141        client_id: Option<ClientId>,
2142        params: Option<Py<PyDict>>,
2143    ) -> PyResult<()> {
2144        self.ensure_registered()?;
2145        let params = dict_to_params(py, params)?;
2146        DataActor::unsubscribe_book_deltas(self.inner_mut(), instrument_id, client_id, params);
2147        Ok(())
2148    }
2149
2150    #[pyo3(name = "unsubscribe_book_depth")]
2151    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2152    fn py_unsubscribe_book_depth(
2153        &mut self,
2154        py: Python<'_>,
2155        instrument_id: InstrumentId,
2156        client_id: Option<ClientId>,
2157        params: Option<Py<PyDict>>,
2158    ) -> PyResult<()> {
2159        self.ensure_registered()?;
2160        let params = dict_to_params(py, params)?;
2161        DataActor::unsubscribe_book_depth(self.inner_mut(), instrument_id, client_id, params);
2162        Ok(())
2163    }
2164
2165    #[pyo3(name = "unsubscribe_book_at_interval")]
2166    #[pyo3(signature = (instrument_id, interval_ms, client_id=None, params=None))]
2167    fn py_unsubscribe_book_at_interval(
2168        &mut self,
2169        py: Python<'_>,
2170        instrument_id: InstrumentId,
2171        interval_ms: usize,
2172        client_id: Option<ClientId>,
2173        params: Option<Py<PyDict>>,
2174    ) -> PyResult<()> {
2175        let interval_ms = NonZeroUsize::new(interval_ms)
2176            .ok_or_else(|| to_pyvalue_err("interval_ms must be > 0"))?;
2177
2178        self.ensure_registered()?;
2179        let params = dict_to_params(py, params)?;
2180        DataActor::unsubscribe_book_at_interval(
2181            self.inner_mut(),
2182            instrument_id,
2183            interval_ms,
2184            client_id,
2185            params,
2186        );
2187        Ok(())
2188    }
2189
2190    #[pyo3(name = "unsubscribe_quotes")]
2191    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2192    fn py_unsubscribe_quotes(
2193        &mut self,
2194        py: Python<'_>,
2195        instrument_id: InstrumentId,
2196        client_id: Option<ClientId>,
2197        params: Option<Py<PyDict>>,
2198    ) -> PyResult<()> {
2199        self.ensure_registered()?;
2200        let params = dict_to_params(py, params)?;
2201        DataActor::unsubscribe_quotes(self.inner_mut(), instrument_id, client_id, params);
2202        Ok(())
2203    }
2204
2205    #[pyo3(name = "unsubscribe_trades")]
2206    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2207    fn py_unsubscribe_trades(
2208        &mut self,
2209        py: Python<'_>,
2210        instrument_id: InstrumentId,
2211        client_id: Option<ClientId>,
2212        params: Option<Py<PyDict>>,
2213    ) -> PyResult<()> {
2214        self.ensure_registered()?;
2215        let params = dict_to_params(py, params)?;
2216        DataActor::unsubscribe_trades(self.inner_mut(), instrument_id, client_id, params);
2217        Ok(())
2218    }
2219
2220    #[pyo3(name = "unsubscribe_bars")]
2221    #[pyo3(signature = (bar_type, client_id=None, params=None))]
2222    fn py_unsubscribe_bars(
2223        &mut self,
2224        py: Python<'_>,
2225        bar_type: BarType,
2226        client_id: Option<ClientId>,
2227        params: Option<Py<PyDict>>,
2228    ) -> PyResult<()> {
2229        self.ensure_registered()?;
2230        let params = dict_to_params(py, params)?;
2231        DataActor::unsubscribe_bars(self.inner_mut(), bar_type, client_id, params);
2232        Ok(())
2233    }
2234
2235    #[pyo3(name = "unsubscribe_mark_prices")]
2236    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2237    fn py_unsubscribe_mark_prices(
2238        &mut self,
2239        py: Python<'_>,
2240        instrument_id: InstrumentId,
2241        client_id: Option<ClientId>,
2242        params: Option<Py<PyDict>>,
2243    ) -> PyResult<()> {
2244        self.ensure_registered()?;
2245        let params = dict_to_params(py, params)?;
2246        DataActor::unsubscribe_mark_prices(self.inner_mut(), instrument_id, client_id, params);
2247        Ok(())
2248    }
2249
2250    #[pyo3(name = "unsubscribe_index_prices")]
2251    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2252    fn py_unsubscribe_index_prices(
2253        &mut self,
2254        py: Python<'_>,
2255        instrument_id: InstrumentId,
2256        client_id: Option<ClientId>,
2257        params: Option<Py<PyDict>>,
2258    ) -> PyResult<()> {
2259        self.ensure_registered()?;
2260        let params = dict_to_params(py, params)?;
2261        DataActor::unsubscribe_index_prices(self.inner_mut(), instrument_id, client_id, params);
2262        Ok(())
2263    }
2264
2265    #[pyo3(name = "unsubscribe_funding_rates")]
2266    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2267    fn py_unsubscribe_funding_rates(
2268        &mut self,
2269        py: Python<'_>,
2270        instrument_id: InstrumentId,
2271        client_id: Option<ClientId>,
2272        params: Option<Py<PyDict>>,
2273    ) -> PyResult<()> {
2274        self.ensure_registered()?;
2275        let params = dict_to_params(py, params)?;
2276        DataActor::unsubscribe_funding_rates(self.inner_mut(), instrument_id, client_id, params);
2277        Ok(())
2278    }
2279
2280    #[pyo3(name = "unsubscribe_option_greeks")]
2281    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2282    fn py_unsubscribe_option_greeks(
2283        &mut self,
2284        py: Python<'_>,
2285        instrument_id: InstrumentId,
2286        client_id: Option<ClientId>,
2287        params: Option<Py<PyDict>>,
2288    ) -> PyResult<()> {
2289        self.ensure_registered()?;
2290        let params = dict_to_params(py, params)?;
2291        DataActor::unsubscribe_option_greeks(self.inner_mut(), instrument_id, client_id, params);
2292        Ok(())
2293    }
2294
2295    #[pyo3(name = "unsubscribe_instrument_status")]
2296    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2297    fn py_unsubscribe_instrument_status(
2298        &mut self,
2299        py: Python<'_>,
2300        instrument_id: InstrumentId,
2301        client_id: Option<ClientId>,
2302        params: Option<Py<PyDict>>,
2303    ) -> PyResult<()> {
2304        self.ensure_registered()?;
2305        let params = dict_to_params(py, params)?;
2306        DataActor::unsubscribe_instrument_status(
2307            self.inner_mut(),
2308            instrument_id,
2309            client_id,
2310            params,
2311        );
2312        Ok(())
2313    }
2314
2315    #[pyo3(name = "unsubscribe_instrument_close")]
2316    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2317    fn py_unsubscribe_instrument_close(
2318        &mut self,
2319        py: Python<'_>,
2320        instrument_id: InstrumentId,
2321        client_id: Option<ClientId>,
2322        params: Option<Py<PyDict>>,
2323    ) -> PyResult<()> {
2324        self.ensure_registered()?;
2325        let params = dict_to_params(py, params)?;
2326        DataActor::unsubscribe_instrument_close(self.inner_mut(), instrument_id, client_id, params);
2327        Ok(())
2328    }
2329
2330    #[pyo3(name = "unsubscribe_option_chain")]
2331    #[pyo3(signature = (series_id, client_id=None))]
2332    fn py_unsubscribe_option_chain(
2333        &mut self,
2334        series_id: OptionSeriesId,
2335        client_id: Option<ClientId>,
2336    ) -> PyResult<()> {
2337        self.ensure_registered()?;
2338        DataActor::unsubscribe_option_chain(self.inner_mut(), series_id, client_id);
2339        Ok(())
2340    }
2341
2342    #[pyo3(name = "request_data")]
2343    #[pyo3(signature = (data_type, client_id, start=None, end=None, limit=None, params=None))]
2344    #[expect(clippy::too_many_arguments)]
2345    fn py_request_data(
2346        &mut self,
2347        py: Python<'_>,
2348        data_type: DataType,
2349        client_id: ClientId,
2350        start: Option<Timestamp>,
2351        end: Option<Timestamp>,
2352        limit: Option<usize>,
2353        params: Option<Py<PyDict>>,
2354    ) -> PyResult<String> {
2355        self.ensure_registered_for_data()?;
2356        let params = dict_to_params(py, params)?;
2357        let limit = limit.and_then(NonZeroUsize::new);
2358        let request_id = DataActor::request_data(
2359            self.inner_mut(),
2360            data_type,
2361            client_id,
2362            start,
2363            end,
2364            limit,
2365            params,
2366        )
2367        .map_err(to_pyvalue_err)?;
2368        Ok(request_id.to_string())
2369    }
2370
2371    #[pyo3(name = "request_instrument")]
2372    #[pyo3(signature = (instrument_id, start=None, end=None, client_id=None, params=None))]
2373    fn py_request_instrument(
2374        &mut self,
2375        py: Python<'_>,
2376        instrument_id: InstrumentId,
2377        start: Option<Timestamp>,
2378        end: Option<Timestamp>,
2379        client_id: Option<ClientId>,
2380        params: Option<Py<PyDict>>,
2381    ) -> PyResult<String> {
2382        self.ensure_registered_for_data()?;
2383        let params = dict_to_params(py, params)?;
2384        let request_id = DataActor::request_instrument(
2385            self.inner_mut(),
2386            instrument_id,
2387            start,
2388            end,
2389            client_id,
2390            params,
2391        )
2392        .map_err(to_pyvalue_err)?;
2393        Ok(request_id.to_string())
2394    }
2395
2396    #[pyo3(name = "request_instruments")]
2397    #[pyo3(signature = (venue=None, start=None, end=None, client_id=None, params=None))]
2398    fn py_request_instruments(
2399        &mut self,
2400        py: Python<'_>,
2401        venue: Option<Venue>,
2402        start: Option<Timestamp>,
2403        end: Option<Timestamp>,
2404        client_id: Option<ClientId>,
2405        params: Option<Py<PyDict>>,
2406    ) -> PyResult<String> {
2407        self.ensure_registered_for_data()?;
2408        let params = dict_to_params(py, params)?;
2409        let request_id =
2410            DataActor::request_instruments(self.inner_mut(), venue, start, end, client_id, params)
2411                .map_err(to_pyvalue_err)?;
2412        Ok(request_id.to_string())
2413    }
2414
2415    #[pyo3(name = "request_book_snapshot")]
2416    #[pyo3(signature = (instrument_id, depth=None, client_id=None, params=None))]
2417    fn py_request_book_snapshot(
2418        &mut self,
2419        py: Python<'_>,
2420        instrument_id: InstrumentId,
2421        depth: Option<usize>,
2422        client_id: Option<ClientId>,
2423        params: Option<Py<PyDict>>,
2424    ) -> PyResult<String> {
2425        self.ensure_registered_for_data()?;
2426        let params = dict_to_params(py, params)?;
2427        let depth = depth.and_then(NonZeroUsize::new);
2428
2429        let request_id = DataActor::request_book_snapshot(
2430            self.inner_mut(),
2431            instrument_id,
2432            depth,
2433            client_id,
2434            params,
2435        )
2436        .map_err(to_pyvalue_err)?;
2437        Ok(request_id.to_string())
2438    }
2439
2440    #[pyo3(name = "request_book_deltas")]
2441    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None))]
2442    #[expect(clippy::too_many_arguments)]
2443    fn py_request_book_deltas(
2444        &mut self,
2445        py: Python<'_>,
2446        instrument_id: InstrumentId,
2447        start: Option<Timestamp>,
2448        end: Option<Timestamp>,
2449        limit: Option<usize>,
2450        client_id: Option<ClientId>,
2451        params: Option<Py<PyDict>>,
2452    ) -> PyResult<String> {
2453        self.ensure_registered_for_data()?;
2454        let params = dict_to_params(py, params)?;
2455        let limit = limit.and_then(NonZeroUsize::new);
2456        let request_id = DataActor::request_book_deltas(
2457            self.inner_mut(),
2458            instrument_id,
2459            start,
2460            end,
2461            limit,
2462            client_id,
2463            params,
2464        )
2465        .map_err(to_pyvalue_err)?;
2466        Ok(request_id.to_string())
2467    }
2468
2469    #[pyo3(name = "request_book_depth")]
2470    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, depth=None, client_id=None, params=None))]
2471    #[expect(clippy::too_many_arguments)]
2472    fn py_request_book_depth(
2473        &mut self,
2474        py: Python<'_>,
2475        instrument_id: InstrumentId,
2476        start: Option<Timestamp>,
2477        end: Option<Timestamp>,
2478        limit: Option<usize>,
2479        depth: Option<usize>,
2480        client_id: Option<ClientId>,
2481        params: Option<Py<PyDict>>,
2482    ) -> PyResult<String> {
2483        self.ensure_registered_for_data()?;
2484        let params = dict_to_params(py, params)?;
2485        let limit = limit.and_then(NonZeroUsize::new);
2486        let depth = depth.and_then(NonZeroUsize::new);
2487        let request_id = DataActor::request_book_depth(
2488            self.inner_mut(),
2489            instrument_id,
2490            start,
2491            end,
2492            limit,
2493            depth,
2494            client_id,
2495            params,
2496        )
2497        .map_err(to_pyvalue_err)?;
2498        Ok(request_id.to_string())
2499    }
2500
2501    #[pyo3(name = "request_quotes")]
2502    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None))]
2503    #[expect(clippy::too_many_arguments)]
2504    fn py_request_quotes(
2505        &mut self,
2506        py: Python<'_>,
2507        instrument_id: InstrumentId,
2508        start: Option<Timestamp>,
2509        end: Option<Timestamp>,
2510        limit: Option<usize>,
2511        client_id: Option<ClientId>,
2512        params: Option<Py<PyDict>>,
2513    ) -> PyResult<String> {
2514        self.ensure_registered_for_data()?;
2515        let params = dict_to_params(py, params)?;
2516        let limit = limit.and_then(NonZeroUsize::new);
2517        let request_id = DataActor::request_quotes(
2518            self.inner_mut(),
2519            instrument_id,
2520            start,
2521            end,
2522            limit,
2523            client_id,
2524            params,
2525        )
2526        .map_err(to_pyvalue_err)?;
2527        Ok(request_id.to_string())
2528    }
2529
2530    #[pyo3(name = "request_trades")]
2531    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None))]
2532    #[expect(clippy::too_many_arguments)]
2533    fn py_request_trades(
2534        &mut self,
2535        py: Python<'_>,
2536        instrument_id: InstrumentId,
2537        start: Option<Timestamp>,
2538        end: Option<Timestamp>,
2539        limit: Option<usize>,
2540        client_id: Option<ClientId>,
2541        params: Option<Py<PyDict>>,
2542    ) -> PyResult<String> {
2543        self.ensure_registered_for_data()?;
2544        let params = dict_to_params(py, params)?;
2545        let limit = limit.and_then(NonZeroUsize::new);
2546        let request_id = DataActor::request_trades(
2547            self.inner_mut(),
2548            instrument_id,
2549            start,
2550            end,
2551            limit,
2552            client_id,
2553            params,
2554        )
2555        .map_err(to_pyvalue_err)?;
2556        Ok(request_id.to_string())
2557    }
2558
2559    #[pyo3(name = "request_funding_rates")]
2560    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None))]
2561    #[expect(clippy::too_many_arguments)]
2562    fn py_request_funding_rates(
2563        &mut self,
2564        py: Python<'_>,
2565        instrument_id: InstrumentId,
2566        start: Option<Timestamp>,
2567        end: Option<Timestamp>,
2568        limit: Option<usize>,
2569        client_id: Option<ClientId>,
2570        params: Option<Py<PyDict>>,
2571    ) -> PyResult<String> {
2572        self.ensure_registered_for_data()?;
2573        let params = dict_to_params(py, params)?;
2574        let limit = limit.and_then(NonZeroUsize::new);
2575        let request_id = DataActor::request_funding_rates(
2576            self.inner_mut(),
2577            instrument_id,
2578            start,
2579            end,
2580            limit,
2581            client_id,
2582            params,
2583        )
2584        .map_err(to_pyvalue_err)?;
2585        Ok(request_id.to_string())
2586    }
2587
2588    #[pyo3(name = "request_bars")]
2589    #[pyo3(signature = (bar_type, start=None, end=None, limit=None, client_id=None, params=None))]
2590    #[expect(clippy::too_many_arguments)]
2591    fn py_request_bars(
2592        &mut self,
2593        py: Python<'_>,
2594        bar_type: BarType,
2595        start: Option<Timestamp>,
2596        end: Option<Timestamp>,
2597        limit: Option<usize>,
2598        client_id: Option<ClientId>,
2599        params: Option<Py<PyDict>>,
2600    ) -> PyResult<String> {
2601        self.ensure_registered_for_data()?;
2602        let params = dict_to_params(py, params)?;
2603        let limit = limit.and_then(NonZeroUsize::new);
2604        let request_id = DataActor::request_bars(
2605            self.inner_mut(),
2606            bar_type,
2607            start,
2608            end,
2609            limit,
2610            client_id,
2611            params,
2612        )
2613        .map_err(to_pyvalue_err)?;
2614        Ok(request_id.to_string())
2615    }
2616
2617    /// Requests reconnect of one socket endpoint owned by `client_id`.
2618    #[pyo3(name = "reconnect_socket")]
2619    fn py_reconnect_socket(&self, client_id: ClientId, endpoint: &str) -> PyResult<()> {
2620        DataActor::reconnect_socket(self.inner(), client_id, endpoint).map_err(to_pyruntime_err)
2621    }
2622
2623    #[allow(unused_variables, clippy::needless_pass_by_value)]
2624    #[pyo3(name = "on_historical_data")]
2625    fn py_on_historical_data(_slf: &Bound<'_, Self>, data: Py<PyAny>) {
2626        // Default implementation - can be overridden in Python subclasses
2627    }
2628
2629    #[allow(unused_variables, clippy::needless_pass_by_value)]
2630    #[pyo3(name = "on_historical_book_deltas")]
2631    fn py_on_historical_book_deltas(_slf: &Bound<'_, Self>, deltas: Vec<OrderBookDelta>) {}
2632
2633    #[allow(unused_variables, clippy::needless_pass_by_value)]
2634    #[pyo3(name = "on_historical_book_depth")]
2635    fn py_on_historical_book_depth(_slf: &Bound<'_, Self>, depths: Vec<OrderBookDepth>) {}
2636
2637    #[allow(unused_variables, clippy::needless_pass_by_value)]
2638    #[pyo3(name = "on_historical_quotes")]
2639    fn py_on_historical_quotes(_slf: &Bound<'_, Self>, quotes: Vec<QuoteTick>) {
2640        // Default implementation - can be overridden in Python subclasses
2641    }
2642
2643    #[allow(unused_variables, clippy::needless_pass_by_value)]
2644    #[pyo3(name = "on_historical_trades")]
2645    fn py_on_historical_trades(_slf: &Bound<'_, Self>, trades: Vec<TradeTick>) {
2646        // Default implementation - can be overridden in Python subclasses
2647    }
2648
2649    #[allow(unused_variables, clippy::needless_pass_by_value)]
2650    #[pyo3(name = "on_historical_funding_rates")]
2651    fn py_on_historical_funding_rates(
2652        _slf: &Bound<'_, Self>,
2653        funding_rates: Vec<FundingRateUpdate>,
2654    ) {
2655        // Default implementation - can be overridden in Python subclasses
2656    }
2657
2658    #[allow(unused_variables, clippy::needless_pass_by_value)]
2659    #[pyo3(name = "on_historical_bars")]
2660    fn py_on_historical_bars(_slf: &Bound<'_, Self>, bars: Vec<Bar>) {
2661        // Default implementation - can be overridden in Python subclasses
2662    }
2663
2664    #[allow(unused_variables, clippy::needless_pass_by_value)]
2665    #[pyo3(name = "on_historical_mark_prices")]
2666    fn py_on_historical_mark_prices(_slf: &Bound<'_, Self>, mark_prices: Vec<MarkPriceUpdate>) {
2667        // Default implementation - can be overridden in Python subclasses
2668    }
2669
2670    #[allow(unused_variables, clippy::needless_pass_by_value)]
2671    #[pyo3(name = "on_historical_index_prices")]
2672    fn py_on_historical_index_prices(_slf: &Bound<'_, Self>, index_prices: Vec<IndexPriceUpdate>) {
2673        // Default implementation - can be overridden in Python subclasses
2674    }
2675}
2676
2677#[pyo3_stub_gen::derive::gen_stub_pymethods]
2678#[pyo3::pymethods]
2679impl PyDataActor {
2680    #[pyo3(name = "publish_message", signature = (topic, message))]
2681    fn py_publish_message(
2682        slf: &Bound<'_, Self>,
2683        topic: &str,
2684        #[gen_stub(override_type(type_repr = "object"))] message: Py<PyAny>,
2685    ) -> PyResult<()> {
2686        let messages = get_python_message_bus(slf.as_any())?;
2687        messages.publish_message(topic, message)
2688    }
2689
2690    #[pyo3(name = "subscribe_topic")]
2691    #[pyo3(signature = (topic, handler, priority=0))]
2692    fn py_subscribe_topic(
2693        slf: &Bound<'_, Self>,
2694        topic: &str,
2695        #[gen_stub(override_type(type_repr = "collections.abc.Callable[[object], None]", imports = ("collections.abc",)))]
2696        handler: Py<PyAny>,
2697        priority: u32,
2698    ) -> PyResult<()> {
2699        let messages = get_python_message_bus(slf.as_any())?;
2700        messages.subscribe_topic(slf.py(), topic, handler, priority)
2701    }
2702
2703    #[pyo3(name = "unsubscribe_topic", signature = (topic, handler))]
2704    fn py_unsubscribe_topic(
2705        slf: &Bound<'_, Self>,
2706        topic: &str,
2707        #[gen_stub(override_type(type_repr = "collections.abc.Callable[[object], None]", imports = ("collections.abc",)))]
2708        handler: &Bound<'_, PyAny>,
2709    ) -> PyResult<()> {
2710        let messages = get_python_message_bus(slf.as_any())?;
2711        messages.unsubscribe_topic(topic, handler)
2712    }
2713}
2714
2715#[cfg(feature = "defi")]
2716#[pymethods]
2717#[pyo3_stub_gen::derive::gen_stub_pymethods]
2718impl PyDataActor {
2719    #[pyo3(name = "on_block")]
2720    #[allow(unused_variables, clippy::needless_pass_by_value)]
2721    fn py_on_block(_slf: &Bound<'_, Self>, block: Block) {}
2722
2723    #[pyo3(name = "on_pool")]
2724    #[allow(unused_variables, clippy::needless_pass_by_value)]
2725    fn py_on_pool(_slf: &Bound<'_, Self>, pool: Pool) {}
2726
2727    #[pyo3(name = "on_pool_swap")]
2728    #[allow(unused_variables, clippy::needless_pass_by_value)]
2729    fn py_on_pool_swap(_slf: &Bound<'_, Self>, swap: PoolSwap) {}
2730
2731    #[pyo3(name = "on_pool_liquidity_update")]
2732    #[allow(unused_variables, clippy::needless_pass_by_value)]
2733    fn py_on_pool_liquidity_update(_slf: &Bound<'_, Self>, update: PoolLiquidityUpdate) {}
2734
2735    #[pyo3(name = "on_pool_fee_collect")]
2736    #[allow(unused_variables, clippy::needless_pass_by_value)]
2737    fn py_on_pool_fee_collect(_slf: &Bound<'_, Self>, update: PoolFeeCollect) {}
2738
2739    #[pyo3(name = "on_pool_flash")]
2740    #[allow(unused_variables, clippy::needless_pass_by_value)]
2741    fn py_on_pool_flash(_slf: &Bound<'_, Self>, flash: PoolFlash) {}
2742
2743    #[pyo3(name = "subscribe_blocks")]
2744    #[pyo3(signature = (chain, client_id=None, params=None))]
2745    fn py_subscribe_blocks(
2746        &mut self,
2747        py: Python<'_>,
2748        chain: Blockchain,
2749        client_id: Option<ClientId>,
2750        params: Option<Py<PyDict>>,
2751    ) -> PyResult<()> {
2752        self.ensure_registered()?;
2753        let params = dict_to_params(py, params)?;
2754        DataActor::subscribe_blocks(self.inner_mut(), chain, client_id, params);
2755        Ok(())
2756    }
2757
2758    #[pyo3(name = "subscribe_pool")]
2759    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2760    fn py_subscribe_pool(
2761        &mut self,
2762        py: Python<'_>,
2763        instrument_id: InstrumentId,
2764        client_id: Option<ClientId>,
2765        params: Option<Py<PyDict>>,
2766    ) -> PyResult<()> {
2767        self.ensure_registered()?;
2768        let params = dict_to_params(py, params)?;
2769        DataActor::subscribe_pool(self.inner_mut(), instrument_id, client_id, params);
2770        Ok(())
2771    }
2772
2773    #[pyo3(name = "subscribe_pool_swaps")]
2774    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2775    fn py_subscribe_pool_swaps(
2776        &mut self,
2777        py: Python<'_>,
2778        instrument_id: InstrumentId,
2779        client_id: Option<ClientId>,
2780        params: Option<Py<PyDict>>,
2781    ) -> PyResult<()> {
2782        self.ensure_registered()?;
2783        let params = dict_to_params(py, params)?;
2784        DataActor::subscribe_pool_swaps(self.inner_mut(), instrument_id, client_id, params);
2785        Ok(())
2786    }
2787
2788    #[pyo3(name = "subscribe_pool_liquidity_updates")]
2789    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2790    fn py_subscribe_pool_liquidity_updates(
2791        &mut self,
2792        py: Python<'_>,
2793        instrument_id: InstrumentId,
2794        client_id: Option<ClientId>,
2795        params: Option<Py<PyDict>>,
2796    ) -> PyResult<()> {
2797        self.ensure_registered()?;
2798        let params = dict_to_params(py, params)?;
2799        DataActor::subscribe_pool_liquidity_updates(
2800            self.inner_mut(),
2801            instrument_id,
2802            client_id,
2803            params,
2804        );
2805        Ok(())
2806    }
2807
2808    #[pyo3(name = "subscribe_pool_fee_collects")]
2809    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2810    fn py_subscribe_pool_fee_collects(
2811        &mut self,
2812        py: Python<'_>,
2813        instrument_id: InstrumentId,
2814        client_id: Option<ClientId>,
2815        params: Option<Py<PyDict>>,
2816    ) -> PyResult<()> {
2817        self.ensure_registered()?;
2818        let params = dict_to_params(py, params)?;
2819        DataActor::subscribe_pool_fee_collects(self.inner_mut(), instrument_id, client_id, params);
2820        Ok(())
2821    }
2822
2823    #[pyo3(name = "subscribe_pool_flash_events")]
2824    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2825    fn py_subscribe_pool_flash_events(
2826        &mut self,
2827        py: Python<'_>,
2828        instrument_id: InstrumentId,
2829        client_id: Option<ClientId>,
2830        params: Option<Py<PyDict>>,
2831    ) -> PyResult<()> {
2832        self.ensure_registered()?;
2833        let params = dict_to_params(py, params)?;
2834        DataActor::subscribe_pool_flash_events(self.inner_mut(), instrument_id, client_id, params);
2835        Ok(())
2836    }
2837
2838    #[pyo3(name = "unsubscribe_blocks")]
2839    #[pyo3(signature = (chain, client_id=None, params=None))]
2840    fn py_unsubscribe_blocks(
2841        &mut self,
2842        py: Python<'_>,
2843        chain: Blockchain,
2844        client_id: Option<ClientId>,
2845        params: Option<Py<PyDict>>,
2846    ) -> PyResult<()> {
2847        self.ensure_registered()?;
2848        let params = dict_to_params(py, params)?;
2849        DataActor::unsubscribe_blocks(self.inner_mut(), chain, client_id, params);
2850        Ok(())
2851    }
2852
2853    #[pyo3(name = "unsubscribe_pool")]
2854    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2855    fn py_unsubscribe_pool(
2856        &mut self,
2857        py: Python<'_>,
2858        instrument_id: InstrumentId,
2859        client_id: Option<ClientId>,
2860        params: Option<Py<PyDict>>,
2861    ) -> PyResult<()> {
2862        self.ensure_registered()?;
2863        let params = dict_to_params(py, params)?;
2864        DataActor::unsubscribe_pool(self.inner_mut(), instrument_id, client_id, params);
2865        Ok(())
2866    }
2867
2868    #[pyo3(name = "unsubscribe_pool_swaps")]
2869    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2870    fn py_unsubscribe_pool_swaps(
2871        &mut self,
2872        py: Python<'_>,
2873        instrument_id: InstrumentId,
2874        client_id: Option<ClientId>,
2875        params: Option<Py<PyDict>>,
2876    ) -> PyResult<()> {
2877        self.ensure_registered()?;
2878        let params = dict_to_params(py, params)?;
2879        DataActor::unsubscribe_pool_swaps(self.inner_mut(), instrument_id, client_id, params);
2880        Ok(())
2881    }
2882
2883    #[pyo3(name = "unsubscribe_pool_liquidity_updates")]
2884    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2885    fn py_unsubscribe_pool_liquidity_updates(
2886        &mut self,
2887        py: Python<'_>,
2888        instrument_id: InstrumentId,
2889        client_id: Option<ClientId>,
2890        params: Option<Py<PyDict>>,
2891    ) -> PyResult<()> {
2892        self.ensure_registered()?;
2893        let params = dict_to_params(py, params)?;
2894        DataActor::unsubscribe_pool_liquidity_updates(
2895            self.inner_mut(),
2896            instrument_id,
2897            client_id,
2898            params,
2899        );
2900        Ok(())
2901    }
2902
2903    #[pyo3(name = "unsubscribe_pool_fee_collects")]
2904    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2905    fn py_unsubscribe_pool_fee_collects(
2906        &mut self,
2907        py: Python<'_>,
2908        instrument_id: InstrumentId,
2909        client_id: Option<ClientId>,
2910        params: Option<Py<PyDict>>,
2911    ) -> PyResult<()> {
2912        self.ensure_registered()?;
2913        let params = dict_to_params(py, params)?;
2914        DataActor::unsubscribe_pool_fee_collects(
2915            self.inner_mut(),
2916            instrument_id,
2917            client_id,
2918            params,
2919        );
2920        Ok(())
2921    }
2922
2923    #[pyo3(name = "unsubscribe_pool_flash_events")]
2924    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2925    fn py_unsubscribe_pool_flash_events(
2926        &mut self,
2927        py: Python<'_>,
2928        instrument_id: InstrumentId,
2929        client_id: Option<ClientId>,
2930        params: Option<Py<PyDict>>,
2931    ) -> PyResult<()> {
2932        self.ensure_registered()?;
2933        let params = dict_to_params(py, params)?;
2934        DataActor::unsubscribe_pool_flash_events(
2935            self.inner_mut(),
2936            instrument_id,
2937            client_id,
2938            params,
2939        );
2940        Ok(())
2941    }
2942}
2943
2944impl PyDataActor {
2945    fn ensure_registered_for_data(&self) -> PyResult<()> {
2946        if self.inner().core.is_registered() {
2947            Ok(())
2948        } else {
2949            Err(to_pyruntime_err(
2950                "DataActor must be registered before publishing, managing synthetics, or requesting data",
2951            ))
2952        }
2953    }
2954
2955    fn ensure_registered(&self) -> PyResult<()> {
2956        if self.inner().core.is_registered() {
2957            Ok(())
2958        } else {
2959            Err(to_pyruntime_err(
2960                "DataActor must be registered before managing subscriptions",
2961            ))
2962        }
2963    }
2964}
2965
2966/// Prepares a Python actor for registration and returns its final actor ID.
2967///
2968/// Applies optional config overrides and stores a weak reference to the Python instance used for
2969/// method dispatch. Missing or unreadable attributes are ignored, and non-boolean `log_events` or
2970/// `log_commands` values leave the existing settings unchanged. When neither the config nor
2971/// `DataActor.__init__` supplies an ID, the runtime class name replaces the shared default so
2972/// subclasses that skip the base initializer still receive distinct IDs.
2973///
2974/// # Errors
2975///
2976/// Returns an error if the actor cannot be extracted, its configured `actor_id` is neither an
2977/// [`ActorId`] nor a valid ID string, its class-derived ID is invalid, or its Python instance
2978/// cannot be weakly referenced.
2979pub fn prepare_python_actor(
2980    actor_obj: &Bound<'_, PyAny>,
2981    config: Option<&Bound<'_, PyAny>>,
2982) -> anyhow::Result<ActorId> {
2983    let mut actor = actor_obj
2984        .extract::<PyRefMut<PyDataActor>>()
2985        .map_err(Into::<PyErr>::into)
2986        .map_err(|e| anyhow::anyhow!("Failed to extract PyDataActor: {e}"))?;
2987
2988    if let Some(config) = config {
2989        if let Some(actor_id) = config
2990            .getattr("actor_id")
2991            .ok()
2992            .filter(|actor_id| !actor_id.is_none())
2993        {
2994            let actor_id = if let Ok(actor_id) = actor_id.extract::<ActorId>() {
2995                actor_id
2996            } else if let Ok(actor_id) = actor_id.extract::<String>() {
2997                ActorId::new_checked(&actor_id)?
2998            } else {
2999                anyhow::bail!("Invalid `actor_id` type");
3000            };
3001            actor.set_actor_id(actor_id);
3002        }
3003
3004        if let Some(log_events) = extract_bool_config_attr(config, "log_events") {
3005            actor.set_log_events(log_events);
3006        }
3007
3008        if let Some(log_commands) = extract_bool_config_attr(config, "log_commands") {
3009            actor.set_log_commands(log_commands);
3010        }
3011    }
3012
3013    actor.set_python_instance(actor_obj)?;
3014    apply_class_derived_actor_id(&mut actor, actor_obj)?;
3015    Ok(actor.actor_id())
3016}
3017
3018fn apply_class_derived_actor_id(
3019    actor: &mut PyRefMut<'_, PyDataActor>,
3020    actor_obj: &Bound<'_, PyAny>,
3021) -> PyResult<()> {
3022    if actor.inner().core.config.actor_id.is_some() {
3023        return Ok(());
3024    }
3025
3026    let py_type = actor_obj.get_type();
3027    let type_name = py_type.name()?;
3028    let actor_id = ActorId::new_checked(type_name.to_str()?).map_err(to_pyvalue_err)?;
3029    actor.set_actor_id(actor_id);
3030
3031    Ok(())
3032}
3033
3034fn extract_bool_config_attr(config: &Bound<'_, PyAny>, attr: &str) -> Option<bool> {
3035    config
3036        .getattr(attr)
3037        .ok()
3038        .and_then(|value| value.extract::<bool>().ok())
3039}
3040
3041fn borrow_actor_mut<'py>(
3042    actor: &Bound<'py, PyDataActor>,
3043    operation: &'static str,
3044) -> PyResult<PyRefMut<'py, PyDataActor>> {
3045    actor.try_borrow_mut().map_err(|_| {
3046        to_pyruntime_err(ComponentAccessError::WriteConflict {
3047            resource: "Python actor",
3048            operation,
3049        })
3050    })
3051}
3052
3053/// Returns whether the config retained by the actor supplies an actor ID.
3054///
3055/// The config is read through Python rather than the extracted [`DataActorConfig`] so that a
3056/// custom subclass config which cannot be extracted still counts as configuring an ID. The actor
3057/// borrow is released before the attribute lookup, which can run user code.
3058fn has_configured_actor_id(slf: &Bound<'_, PyDataActor>) -> bool {
3059    let py = slf.py();
3060    let config = slf
3061        .borrow()
3062        .inner()
3063        .config
3064        .as_ref()
3065        .map(|config| config.clone_ref(py));
3066
3067    config.is_some_and(|config| {
3068        config
3069            .bind(py)
3070            .getattr("actor_id")
3071            .is_ok_and(|actor_id| !actor_id.is_none())
3072    })
3073}
3074
3075#[cfg(test)]
3076mod tests {
3077    use std::{cell::RefCell, collections::HashMap, rc::Rc, str::FromStr, sync::Arc};
3078
3079    #[cfg(feature = "defi")]
3080    use alloy_primitives::{I256, U160, U256};
3081    use indexmap::IndexMap;
3082    use nautilus_core::{UUID4, UnixNanos, python::IntoPyObjectNautilusExt};
3083    #[cfg(feature = "defi")]
3084    use nautilus_model::defi::{
3085        AmmType, Block, Blockchain, Chain, Dex, DexType, Pool, PoolFeeCollect, PoolFlash,
3086        PoolIdentifier, PoolLiquidityUpdate, PoolLiquidityUpdateType, PoolSwap, Token,
3087    };
3088    use nautilus_model::{
3089        data::{
3090            Bar, BarType, CustomData, DataType, FundingRateUpdate, IndexPriceUpdate,
3091            InstrumentStatus, MarkPriceUpdate, OrderBookDelta, OrderBookDeltas, OrderBookDepth,
3092            QuoteTick, TradeTick,
3093            close::InstrumentClose,
3094            greeks::OptionGreekValues,
3095            option_chain::{OptionChainSlice, OptionGreeks},
3096            stubs::{stub_custom_data, stub_deltas, stub_depth10},
3097        },
3098        enums::{
3099            AggressorSide, BookType, GreeksConvention, InstrumentCloseType, MarketStatusAction,
3100        },
3101        identifiers::{ActorId, ClientId, ComponentId, OptionSeriesId, TradeId, TraderId, Venue},
3102        instruments::{CurrencyPair, InstrumentAny, stubs::audusd_sim},
3103        orderbook::OrderBook,
3104        types::{Price, Quantity},
3105    };
3106    use pyo3::{
3107        Bound, IntoPyObjectExt, Py, PyAny, PyRef, PyResult, Python,
3108        ffi::c_str,
3109        types::{PyAnyMethods, PyBytes, PyDict, PyList, PyWeakrefMethods, PyWeakrefReference},
3110    };
3111    use rstest::{fixture, rstest};
3112    use ustr::Ustr;
3113
3114    use super::{PyDataActor, prepare_python_actor};
3115    use crate::{
3116        actor::{DataActor, data_actor::DataActorConfig, registry::actor_exists},
3117        cache::Cache,
3118        clock::VirtualClock,
3119        component::{Component, get_component},
3120        enums::ComponentState,
3121        live::runner::replace_system_command_sender,
3122        messages::{
3123            SystemCommand,
3124            data::{BarsResponse, CustomDataResponse, QuotesResponse, TradesResponse},
3125            system::{
3126                QueueCondition, QueueState, QueueStateChanged, SocketState, SocketStateChanged,
3127            },
3128        },
3129        msgbus::{self, MessageBus, MessagingSwitchboard, get_message_bus},
3130        python::wrappers::get_python_wrapper,
3131        runner::{SyncDataCommandSender, SystemChannel, set_data_cmd_sender},
3132        signal::Signal,
3133        timer::TimeEvent,
3134    };
3135
3136    #[fixture]
3137    fn clock() -> Rc<RefCell<VirtualClock>> {
3138        Rc::new(RefCell::new(VirtualClock::new()))
3139    }
3140
3141    #[fixture]
3142    fn cache() -> Rc<RefCell<Cache>> {
3143        Rc::new(RefCell::new(Cache::new(None, None)))
3144    }
3145
3146    #[fixture]
3147    fn trader_id() -> TraderId {
3148        TraderId::from("TRADER-001")
3149    }
3150
3151    #[fixture]
3152    fn client_id() -> ClientId {
3153        ClientId::new("TestClient")
3154    }
3155
3156    #[fixture]
3157    fn venue() -> Venue {
3158        Venue::from("SIM")
3159    }
3160
3161    #[fixture]
3162    fn data_type() -> DataType {
3163        DataType::new("TestData", None, None)
3164    }
3165
3166    #[fixture]
3167    fn bar_type(audusd_sim: CurrencyPair) -> BarType {
3168        BarType::from_str(&format!("{}-1-MINUTE-LAST-INTERNAL", audusd_sim.id)).unwrap()
3169    }
3170
3171    fn create_unregistered_actor() -> PyDataActor {
3172        PyDataActor::new(None)
3173    }
3174
3175    fn create_registered_actor(
3176        clock: Rc<RefCell<VirtualClock>>,
3177        cache: Rc<RefCell<Cache>>,
3178        trader_id: TraderId,
3179    ) -> PyDataActor {
3180        // Set up sync data command sender for tests
3181        let sender = SyncDataCommandSender;
3182        set_data_cmd_sender(Arc::new(sender));
3183
3184        let mut actor = PyDataActor::new(None);
3185        actor.register(trader_id, clock, cache).unwrap();
3186        actor
3187    }
3188
3189    #[rstest]
3190    fn test_new_actor_creation() {
3191        let actor = PyDataActor::new(None);
3192        assert!(actor.trader_id().is_none());
3193    }
3194
3195    #[rstest]
3196    fn test_actor_retains_python_config_object() {
3197        pyo3::Python::initialize();
3198        Python::attach(|py| {
3199            let config = py
3200                .eval(
3201                    c_str!("type('_Cfg', (), {'actor_id': 'A-RETAIN-001'})()"),
3202                    None,
3203                    None,
3204                )
3205                .unwrap();
3206
3207            let actor = py
3208                .get_type::<PyDataActor>()
3209                .as_any()
3210                .call1((config.clone(),))
3211                .unwrap();
3212
3213            let retained = actor.getattr("config").unwrap();
3214
3215            assert!(retained.is(&config));
3216        });
3217    }
3218
3219    #[rstest]
3220    fn test_prepare_python_actor_applies_shared_registration_boundary() {
3221        pyo3::Python::initialize();
3222
3223        Python::attach(|py| {
3224            let namespace = PyDict::new(py);
3225            namespace
3226                .set_item("DataActor", py.get_type::<PyDataActor>())
3227                .unwrap();
3228            py.run(
3229                c_str!(
3230                    r#"
3231class PreparedActor(DataActor):
3232    def __init__(self, _config=None):
3233        pass
3234"#
3235                ),
3236                Some(&namespace),
3237                None,
3238            )
3239            .unwrap();
3240            let actor_class = namespace.get_item("PreparedActor").unwrap();
3241
3242            let configured = py
3243                .eval(
3244                    c_str!(
3245                        "type('_Cfg', (), {'actor_id': 'CONFIGURED-001', 'log_events': False, 'log_commands': False})()"
3246                    ),
3247                    None,
3248                    None,
3249                )
3250                .unwrap();
3251            let configured_actor = actor_class.call1((configured.clone(),)).unwrap();
3252            let configured_id = prepare_python_actor(&configured_actor, Some(&configured)).unwrap();
3253            let configured_ref = configured_actor.extract::<PyRef<PyDataActor>>().unwrap();
3254            let configured_wrapper = configured_ref.inner().python_instance().unwrap().unwrap();
3255
3256            let fallback_actor = actor_class.call0().unwrap();
3257            let fallback_id = prepare_python_actor(&fallback_actor, None).unwrap();
3258            let fallback_ref = fallback_actor.extract::<PyRef<PyDataActor>>().unwrap();
3259
3260            let invalid_logging = py
3261                .eval(
3262                    c_str!(
3263                        "type('_Cfg', (), {'actor_id': None, 'log_events': object(), 'log_commands': object()})()"
3264                    ),
3265                    None,
3266                    None,
3267                )
3268                .unwrap();
3269            let invalid_logging_actor = actor_class.call1((invalid_logging.clone(),)).unwrap();
3270            let invalid_logging_id =
3271                prepare_python_actor(&invalid_logging_actor, Some(&invalid_logging)).unwrap();
3272            let invalid_logging_ref = invalid_logging_actor
3273                .extract::<PyRef<PyDataActor>>()
3274                .unwrap();
3275
3276            let invalid = py
3277                .eval(
3278                    c_str!("type('_Cfg', (), {'actor_id': object()})()"),
3279                    None,
3280                    None,
3281                )
3282                .unwrap();
3283            let invalid_actor = actor_class.call1((invalid.clone(),)).unwrap();
3284            let error = prepare_python_actor(&invalid_actor, Some(&invalid)).unwrap_err();
3285
3286            assert_eq!(configured_id, ActorId::from("CONFIGURED-001"));
3287            assert_eq!(configured_ref.config.actor_id, Some(configured_id));
3288            assert!(!configured_ref.config.log_events);
3289            assert!(!configured_ref.config.log_commands);
3290            assert!(configured_wrapper.bind(py).is(&configured_actor));
3291            assert_eq!(fallback_id, ActorId::from("PreparedActor"));
3292            assert_eq!(fallback_ref.config.actor_id, Some(fallback_id));
3293            assert_eq!(invalid_logging_id, ActorId::from("PreparedActor"));
3294            assert!(invalid_logging_ref.config.log_events);
3295            assert!(invalid_logging_ref.config.log_commands);
3296            assert_eq!(error.to_string(), "Invalid `actor_id` type");
3297        });
3298    }
3299
3300    #[rstest]
3301    fn test_clock_access_before_registration_raises_error() {
3302        let actor = PyDataActor::new(None);
3303
3304        // Accessing clock before registration should raise PyRuntimeError
3305        let result = actor.py_clock();
3306        assert!(result.is_err());
3307
3308        let error = result.unwrap_err();
3309        pyo3::Python::initialize();
3310
3311        pyo3::Python::attach(|py| {
3312            assert!(error.is_instance_of::<pyo3::exceptions::PyRuntimeError>(py));
3313        });
3314
3315        let error_msg = error.to_string();
3316        assert!(
3317            error_msg.contains("Actor must be registered with a trader before accessing clock")
3318        );
3319    }
3320
3321    #[rstest]
3322    fn test_unregistered_actor_methods_work() {
3323        let actor = create_unregistered_actor();
3324
3325        assert!(!actor.py_is_ready());
3326        assert!(!actor.py_is_running());
3327        assert!(!actor.py_is_stopped());
3328        assert!(!actor.py_is_disposed());
3329        assert!(!actor.py_is_degraded());
3330        assert!(!actor.py_is_faulted());
3331
3332        // Verify unregistered state
3333        assert_eq!(actor.trader_id(), None);
3334    }
3335
3336    #[rstest]
3337    fn test_registration_success(
3338        clock: Rc<RefCell<VirtualClock>>,
3339        cache: Rc<RefCell<Cache>>,
3340        trader_id: TraderId,
3341    ) {
3342        let mut actor = create_unregistered_actor();
3343        actor.register(trader_id, clock, cache).unwrap();
3344        assert!(actor.trader_id().is_some());
3345        assert_eq!(actor.trader_id().unwrap(), trader_id);
3346    }
3347
3348    #[rstest]
3349    fn test_registered_actor_basic_properties(
3350        clock: Rc<RefCell<VirtualClock>>,
3351        cache: Rc<RefCell<Cache>>,
3352        trader_id: TraderId,
3353    ) {
3354        let actor = create_registered_actor(clock, cache, trader_id);
3355
3356        assert_eq!(actor.state(), ComponentState::Ready);
3357        assert_eq!(actor.trader_id(), Some(TraderId::from("TRADER-001")));
3358        assert!(actor.py_is_ready());
3359        assert!(!actor.py_is_running());
3360        assert!(!actor.py_is_stopped());
3361        assert!(!actor.py_is_disposed());
3362        assert!(!actor.py_is_degraded());
3363        assert!(!actor.py_is_faulted());
3364    }
3365
3366    #[rstest]
3367    fn test_basic_subscription_methods_compile(
3368        clock: Rc<RefCell<VirtualClock>>,
3369        cache: Rc<RefCell<Cache>>,
3370        trader_id: TraderId,
3371        data_type: DataType,
3372        client_id: ClientId,
3373        audusd_sim: CurrencyPair,
3374    ) {
3375        let mut actor = create_registered_actor(clock, cache, trader_id);
3376
3377        pyo3::Python::initialize();
3378
3379        pyo3::Python::attach(|py| {
3380            assert!(
3381                actor
3382                    .py_subscribe_data(py, data_type.clone(), Some(client_id), None)
3383                    .is_ok()
3384            );
3385            assert!(
3386                actor
3387                    .py_subscribe_quotes(py, audusd_sim.id, Some(client_id), None)
3388                    .is_ok()
3389            );
3390            assert!(
3391                actor
3392                    .py_unsubscribe_data(py, data_type, Some(client_id), None)
3393                    .is_ok()
3394            );
3395            assert!(
3396                actor
3397                    .py_unsubscribe_quotes(py, audusd_sim.id, Some(client_id), None)
3398                    .is_ok()
3399            );
3400        });
3401    }
3402
3403    #[rstest]
3404    fn test_shutdown_system_passes_through(
3405        clock: Rc<RefCell<VirtualClock>>,
3406        cache: Rc<RefCell<Cache>>,
3407        trader_id: TraderId,
3408    ) {
3409        let actor = create_registered_actor(clock, cache, trader_id);
3410
3411        assert!(
3412            actor
3413                .py_shutdown_system(Some("Test shutdown".to_string()))
3414                .is_ok()
3415        );
3416        assert!(actor.py_shutdown_system(None).is_ok());
3417    }
3418
3419    #[rstest]
3420    fn test_publish_data_delivers_to_any_subscriber(
3421        clock: Rc<RefCell<VirtualClock>>,
3422        cache: Rc<RefCell<Cache>>,
3423        trader_id: TraderId,
3424    ) {
3425        use crate::msgbus::{
3426            self, MessageBus, get_message_bus, switchboard::get_custom_topic,
3427            typed_handler::ShareableMessageHandler,
3428        };
3429
3430        // Ensure clean msgbus for this test
3431        *get_message_bus().borrow_mut() = MessageBus::default();
3432
3433        let actor = create_registered_actor(clock, cache, trader_id);
3434        let data = stub_custom_data(1, 42, None, None);
3435        let topic = get_custom_topic(&data.data_type);
3436
3437        let received: Rc<RefCell<Vec<CustomData>>> = Rc::new(RefCell::new(Vec::new()));
3438        let received_clone = received.clone();
3439        let handler = ShareableMessageHandler::from_typed(move |d: &CustomData| {
3440            received_clone.borrow_mut().push(d.clone());
3441        });
3442        msgbus::subscribe_any(topic.into(), handler, None);
3443
3444        actor.py_publish_data(&data.data_type, &data).unwrap();
3445
3446        let received = received.borrow();
3447        assert_eq!(received.len(), 1);
3448        assert_eq!(received[0].data_type, data.data_type);
3449    }
3450
3451    #[rstest]
3452    fn test_publish_signal_delivers_to_customdata_subscriber(
3453        clock: Rc<RefCell<VirtualClock>>,
3454        cache: Rc<RefCell<Cache>>,
3455        trader_id: TraderId,
3456    ) {
3457        use crate::{
3458            msgbus::{
3459                self, MessageBus, Pattern, get_message_bus, typed_handler::ShareableMessageHandler,
3460            },
3461            signal::Signal,
3462        };
3463
3464        *get_message_bus().borrow_mut() = MessageBus::default();
3465
3466        let actor = create_registered_actor(clock, cache, trader_id);
3467
3468        // Signals travel as `CustomData` on the bus so persistence and other
3469        // `CustomData`-aware subscribers pick them up. Downcast inside the handler.
3470        let received: Rc<RefCell<Vec<Signal>>> = Rc::new(RefCell::new(Vec::new()));
3471        let received_clone = received.clone();
3472        let handler = ShareableMessageHandler::from_typed(move |data: &CustomData| {
3473            if let Some(sig) = data.data.as_any().downcast_ref::<Signal>() {
3474                received_clone.borrow_mut().push(sig.clone());
3475            }
3476        });
3477        let pattern: crate::msgbus::MStr<Pattern> = "data.Signal*".to_string().into();
3478        msgbus::subscribe_any(pattern, handler, None);
3479
3480        pyo3::Python::initialize();
3481
3482        Python::attach(|py| {
3483            let val1: Py<PyAny> = 1.0_f64.into_py_any_unwrap(py);
3484            let val2: Py<PyAny> = "HIGH".into_py_any_unwrap(py);
3485            actor.py_publish_signal(py, "example", val1, 0).unwrap();
3486            actor
3487                .py_publish_signal(py, "risk", val2, 1_700_000_000_000_000_000)
3488                .unwrap();
3489        });
3490
3491        let received = received.borrow();
3492        assert_eq!(received.len(), 2);
3493        assert_eq!(received[0].name, "example");
3494        assert_eq!(received[0].value, "1.0");
3495        assert_eq!(received[1].name, "risk");
3496        assert_eq!(received[1].value, "HIGH");
3497        assert_eq!(
3498            received[1].ts_event,
3499            UnixNanos::from(1_700_000_000_000_000_000_u64)
3500        );
3501    }
3502
3503    #[rstest]
3504    fn test_publish_signal_accepts_numeric_py_values(
3505        clock: Rc<RefCell<VirtualClock>>,
3506        cache: Rc<RefCell<Cache>>,
3507        trader_id: TraderId,
3508    ) {
3509        use crate::{
3510            msgbus::{
3511                self, MessageBus, Pattern, get_message_bus, typed_handler::ShareableMessageHandler,
3512            },
3513            signal::Signal,
3514        };
3515
3516        *get_message_bus().borrow_mut() = MessageBus::default();
3517
3518        let actor = create_registered_actor(clock, cache, trader_id);
3519
3520        let received: Rc<RefCell<Vec<Signal>>> = Rc::new(RefCell::new(Vec::new()));
3521        let received_clone = received.clone();
3522        let handler = ShareableMessageHandler::from_typed(move |data: &CustomData| {
3523            if let Some(sig) = data.data.as_any().downcast_ref::<Signal>() {
3524                received_clone.borrow_mut().push(sig.clone());
3525            }
3526        });
3527        let pattern: crate::msgbus::MStr<Pattern> = "data.Signal*".to_string().into();
3528        msgbus::subscribe_any(pattern, handler, None);
3529
3530        pyo3::Python::initialize();
3531
3532        Python::attach(|py| {
3533            let int_value: Py<PyAny> = 42_i64.into_py_any_unwrap(py);
3534            let float_value: Py<PyAny> = 3.5_f64.into_py_any_unwrap(py);
3535            let bool_value: Py<PyAny> = true.into_py_any_unwrap(py);
3536            actor.py_publish_signal(py, "count", int_value, 0).unwrap();
3537            actor
3538                .py_publish_signal(py, "ratio", float_value, 0)
3539                .unwrap();
3540            actor
3541                .py_publish_signal(py, "active", bool_value, 0)
3542                .unwrap();
3543        });
3544
3545        let received = received.borrow();
3546        assert_eq!(received.len(), 3);
3547        assert_eq!(received[0].value, "42");
3548        assert_eq!(received[1].value, "3.5");
3549        assert_eq!(received[2].value, "True");
3550    }
3551
3552    #[rstest]
3553    fn test_subscribe_and_unsubscribe_signal_compile(
3554        clock: Rc<RefCell<VirtualClock>>,
3555        cache: Rc<RefCell<Cache>>,
3556        trader_id: TraderId,
3557    ) {
3558        use crate::msgbus::{MessageBus, get_message_bus};
3559
3560        *get_message_bus().borrow_mut() = MessageBus::default();
3561
3562        let actor = create_registered_actor(clock, cache, trader_id);
3563        Python::initialize();
3564        Python::attach(|py| {
3565            let actor = Bound::new(py, actor).unwrap();
3566            PyDataActor::py_subscribe_signal(&actor, "example", None).unwrap();
3567            PyDataActor::py_unsubscribe_signal(&actor, "example").unwrap();
3568            PyDataActor::py_subscribe_signal(&actor, "", None).unwrap();
3569            PyDataActor::py_unsubscribe_signal(&actor, "").unwrap();
3570        });
3571    }
3572
3573    #[rstest]
3574    fn test_py_subscribe_signal_forwards_priority(
3575        clock: Rc<RefCell<VirtualClock>>,
3576        cache: Rc<RefCell<Cache>>,
3577        trader_id: TraderId,
3578    ) {
3579        use crate::msgbus::{MessageBus, get_message_bus, switchboard::get_signal_topic};
3580
3581        *get_message_bus().borrow_mut() = MessageBus::default();
3582
3583        let actor = create_registered_actor(clock, cache, trader_id);
3584        Python::initialize();
3585        Python::attach(|py| {
3586            let actor = Bound::new(py, actor).unwrap();
3587            PyDataActor::py_subscribe_signal(&actor, "trigger", Some(50)).unwrap();
3588
3589            // The PyO3 binding must forward the priority to the bus unchanged.
3590            let topic = get_signal_topic("trigger");
3591            let subs = get_message_bus().borrow_mut().matching_subscriptions(topic);
3592            assert_eq!(subs.len(), 1);
3593            assert_eq!(subs[0].priority, 50);
3594        });
3595    }
3596
3597    #[rstest]
3598    fn test_register_in_global_registries_retains_python_wrapper(
3599        clock: Rc<RefCell<VirtualClock>>,
3600        cache: Rc<RefCell<Cache>>,
3601        trader_id: TraderId,
3602    ) {
3603        pyo3::Python::initialize();
3604
3605        Python::attach(|py| {
3606            let py_actor = create_tracking_python_actor(py).unwrap();
3607
3608            let mut rust_actor = PyDataActor::new(Some(DataActorConfig {
3609                actor_id: Some(ActorId::from("RETAINED-ACTOR")),
3610                ..Default::default()
3611            }));
3612            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
3613            rust_actor.register(trader_id, clock, cache).unwrap();
3614
3615            rust_actor.register_in_global_registries().unwrap();
3616
3617            let retained = get_python_wrapper(ComponentId::from("RETAINED-ACTOR"))
3618                .expect("registering must retain the actor's Python wrapper");
3619
3620            assert!(retained.bind(py).is(py_actor.bind(py)));
3621            assert!(get_component(&Ustr::from("RETAINED-ACTOR")).is_some());
3622            assert!(actor_exists(&Ustr::from("RETAINED-ACTOR")));
3623        });
3624    }
3625
3626    #[rstest]
3627    fn test_register_in_global_registries_rejects_missing_python_wrapper(
3628        clock: Rc<RefCell<VirtualClock>>,
3629        cache: Rc<RefCell<Cache>>,
3630        trader_id: TraderId,
3631    ) {
3632        pyo3::Python::initialize();
3633
3634        Python::attach(|_py| {
3635            let mut rust_actor = PyDataActor::new(Some(DataActorConfig {
3636                actor_id: Some(ActorId::from("UNWRAPPED-ACTOR")),
3637                ..Default::default()
3638            }));
3639            rust_actor.register(trader_id, clock, cache).unwrap();
3640
3641            let error = rust_actor
3642                .register_in_global_registries()
3643                .expect_err("registering without a Python wrapper must fail");
3644
3645            assert!(error.to_string().contains("without a Python wrapper"));
3646            assert!(get_component(&Ustr::from("UNWRAPPED-ACTOR")).is_none());
3647            assert!(!actor_exists(&Ustr::from("UNWRAPPED-ACTOR")));
3648            assert!(get_python_wrapper(ComponentId::from("UNWRAPPED-ACTOR")).is_none());
3649        });
3650    }
3651
3652    #[rstest]
3653    fn test_publish_data_dispatches_to_python_on_data(
3654        clock: Rc<RefCell<VirtualClock>>,
3655        cache: Rc<RefCell<Cache>>,
3656        trader_id: TraderId,
3657    ) {
3658        use crate::msgbus::{MessageBus, get_message_bus};
3659
3660        *get_message_bus().borrow_mut() = MessageBus::default();
3661
3662        pyo3::Python::initialize();
3663
3664        Python::attach(|py| {
3665            let py_actor = create_tracking_python_actor(py).unwrap();
3666
3667            let mut rust_actor = PyDataActor::new(None);
3668            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
3669            rust_actor.register(trader_id, clock, cache).unwrap();
3670            rust_actor.register_in_global_registries().unwrap();
3671            rust_actor.py_start().unwrap();
3672
3673            let data = stub_custom_data(1, 42, None, None);
3674            rust_actor
3675                .py_subscribe_data(py, data.data_type.clone(), None, None)
3676                .unwrap();
3677
3678            rust_actor.py_publish_data(&data.data_type, &data).unwrap();
3679            rust_actor.py_publish_data(&data.data_type, &data).unwrap();
3680
3681            assert!(python_method_was_called(&py_actor, py, "on_data"));
3682            assert_eq!(python_method_call_count(&py_actor, py, "on_data"), 2);
3683        });
3684    }
3685
3686    #[rstest]
3687    fn test_publish_signal_dispatches_to_python_on_signal(
3688        clock: Rc<RefCell<VirtualClock>>,
3689        cache: Rc<RefCell<Cache>>,
3690        trader_id: TraderId,
3691    ) {
3692        use crate::msgbus::{MessageBus, get_message_bus};
3693
3694        *get_message_bus().borrow_mut() = MessageBus::default();
3695
3696        pyo3::Python::initialize();
3697
3698        Python::attach(|py| {
3699            let py_actor = create_tracking_python_actor(py).unwrap();
3700
3701            let mut rust_actor = PyDataActor::new(None);
3702            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
3703            rust_actor.register(trader_id, clock, cache).unwrap();
3704            rust_actor.register_in_global_registries().unwrap();
3705            rust_actor.py_start().unwrap();
3706
3707            let bound_actor = Bound::new(py, rust_actor).unwrap();
3708            PyDataActor::py_subscribe_signal(&bound_actor, "example", None).unwrap();
3709            let rust_actor = bound_actor.borrow_mut();
3710            let val1: Py<PyAny> = "1.5".into_py_any_unwrap(py);
3711            let val2: Py<PyAny> = 2.0_f64.into_py_any_unwrap(py);
3712            rust_actor
3713                .py_publish_signal(py, "example", val1, 0)
3714                .unwrap();
3715            rust_actor
3716                .py_publish_signal(py, "example", val2, 1_700_000_000_000_000_000)
3717                .unwrap();
3718
3719            assert!(python_method_was_called(&py_actor, py, "on_signal"));
3720            assert_eq!(python_method_call_count(&py_actor, py, "on_signal"), 2);
3721        });
3722    }
3723
3724    #[rstest]
3725    fn test_unsubscribe_signal_stops_python_dispatch(
3726        clock: Rc<RefCell<VirtualClock>>,
3727        cache: Rc<RefCell<Cache>>,
3728        trader_id: TraderId,
3729    ) {
3730        use crate::msgbus::{MessageBus, get_message_bus};
3731
3732        *get_message_bus().borrow_mut() = MessageBus::default();
3733
3734        pyo3::Python::initialize();
3735
3736        Python::attach(|py| {
3737            let py_actor = create_tracking_python_actor(py).unwrap();
3738
3739            let mut rust_actor = PyDataActor::new(None);
3740            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
3741            rust_actor.register(trader_id, clock, cache).unwrap();
3742            rust_actor.register_in_global_registries().unwrap();
3743            rust_actor.py_start().unwrap();
3744
3745            let bound_actor = Bound::new(py, rust_actor).unwrap();
3746            PyDataActor::py_subscribe_signal(&bound_actor, "example", None).unwrap();
3747            let rust_actor = bound_actor.borrow_mut();
3748            let val1: Py<PyAny> = "1".into_py_any_unwrap(py);
3749            let val2: Py<PyAny> = "2".into_py_any_unwrap(py);
3750            rust_actor
3751                .py_publish_signal(py, "example", val1, 0)
3752                .unwrap();
3753
3754            drop(rust_actor);
3755            PyDataActor::py_unsubscribe_signal(&bound_actor, "example").unwrap();
3756            let rust_actor = bound_actor.borrow_mut();
3757            rust_actor
3758                .py_publish_signal(py, "example", val2, 0)
3759                .unwrap();
3760
3761            assert_eq!(python_method_call_count(&py_actor, py, "on_signal"), 1);
3762        });
3763    }
3764
3765    #[rstest]
3766    #[case(None)]
3767    #[case(Some(SystemChannel::ExecCommands))]
3768    fn test_queue_state_changed_subscription_dispatches_and_unsubscribes(
3769        clock: Rc<RefCell<VirtualClock>>,
3770        cache: Rc<RefCell<Cache>>,
3771        trader_id: TraderId,
3772        #[case] channel: Option<SystemChannel>,
3773    ) {
3774        *get_message_bus().borrow_mut() = MessageBus::default();
3775
3776        pyo3::Python::initialize();
3777
3778        Python::attach(|py| {
3779            let py_actor = create_tracking_python_actor(py).unwrap();
3780
3781            let mut rust_actor = PyDataActor::new(None);
3782            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
3783            rust_actor.register(trader_id, clock, cache).unwrap();
3784            rust_actor.register_in_global_registries().unwrap();
3785            rust_actor.py_start().unwrap();
3786            rust_actor
3787                .py_subscribe_queue_state(channel, Some(50))
3788                .unwrap();
3789
3790            let topic =
3791                MessagingSwitchboard::queue_state_changed_topic(SystemChannel::ExecCommands);
3792            let subscriptions = get_message_bus().borrow_mut().matching_subscriptions(topic);
3793            assert_eq!(subscriptions.len(), 1);
3794            assert_eq!(subscriptions[0].priority, 50);
3795            let unrelated =
3796                MessagingSwitchboard::queue_state_changed_topic(SystemChannel::DataEvents);
3797            assert_eq!(
3798                get_message_bus()
3799                    .borrow_mut()
3800                    .matching_subscriptions(unrelated)
3801                    .len(),
3802                usize::from(channel.is_none())
3803            );
3804
3805            let triggered = sample_queue_state_changed(QueueState::Triggered);
3806            msgbus::publish_any(topic, &triggered);
3807
3808            let (received,) = py_actor
3809                .call_method1(py, "last_call_args", ("on_queue_state",))
3810                .unwrap()
3811                .extract::<(QueueStateChanged,)>(py)
3812                .unwrap();
3813            assert_eq!(received, triggered);
3814
3815            rust_actor.py_unsubscribe_queue_state(channel).unwrap();
3816            let cleared = sample_queue_state_changed(QueueState::Cleared);
3817            msgbus::publish_any(topic, &cleared);
3818
3819            assert_eq!(python_method_call_count(&py_actor, py, "on_queue_state"), 1);
3820        });
3821    }
3822
3823    #[rstest]
3824    #[case(None, None)]
3825    #[case(Some(ClientId::from("BINANCE")), None)]
3826    #[case(None, Some("binance-futures-market-streams"))]
3827    #[case(
3828        Some(ClientId::from("BINANCE")),
3829        Some("binance-futures-market-streams")
3830    )]
3831    fn test_socket_state_changed_subscription_dispatches_and_unsubscribes(
3832        clock: Rc<RefCell<VirtualClock>>,
3833        cache: Rc<RefCell<Cache>>,
3834        trader_id: TraderId,
3835        #[case] client_id: Option<ClientId>,
3836        #[case] endpoint: Option<&str>,
3837    ) {
3838        *get_message_bus().borrow_mut() = MessageBus::default();
3839
3840        pyo3::Python::initialize();
3841
3842        Python::attach(|py| {
3843            let py_actor = create_tracking_python_actor(py).unwrap();
3844
3845            let mut rust_actor = PyDataActor::new(None);
3846            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
3847            rust_actor.register(trader_id, clock, cache).unwrap();
3848            rust_actor.register_in_global_registries().unwrap();
3849            rust_actor.py_start().unwrap();
3850            rust_actor
3851                .py_subscribe_socket_state(client_id, endpoint, Some(50))
3852                .unwrap();
3853
3854            let topic = MessagingSwitchboard::socket_state_changed_topic(
3855                ClientId::from("BINANCE"),
3856                "binance-futures-market-streams",
3857            );
3858            let subscriptions = get_message_bus().borrow_mut().matching_subscriptions(topic);
3859            assert_eq!(subscriptions.len(), 1);
3860            assert_eq!(subscriptions[0].priority, 50);
3861            let unrelated =
3862                MessagingSwitchboard::socket_state_changed_topic(ClientId::from("BYBIT"), "orders");
3863            assert_eq!(
3864                get_message_bus()
3865                    .borrow_mut()
3866                    .matching_subscriptions(unrelated)
3867                    .len(),
3868                usize::from(client_id.is_none() && endpoint.is_none())
3869            );
3870
3871            let connected = sample_socket_state_changed(SocketState::Connected);
3872            msgbus::publish_any(topic, &connected);
3873
3874            let (received,) = py_actor
3875                .call_method1(py, "last_call_args", ("on_socket_state",))
3876                .unwrap()
3877                .extract::<(SocketStateChanged,)>(py)
3878                .unwrap();
3879            assert_eq!(received, connected);
3880
3881            rust_actor
3882                .py_unsubscribe_socket_state(client_id, endpoint)
3883                .unwrap();
3884            let disconnected = sample_socket_state_changed(SocketState::Disconnected);
3885            msgbus::publish_any(topic, &disconnected);
3886
3887            assert_eq!(
3888                python_method_call_count(&py_actor, py, "on_socket_state"),
3889                1
3890            );
3891        });
3892    }
3893
3894    #[rstest]
3895    fn test_subscribe_signal_wildcard_dispatches_all_names_to_python(
3896        clock: Rc<RefCell<VirtualClock>>,
3897        cache: Rc<RefCell<Cache>>,
3898        trader_id: TraderId,
3899    ) {
3900        use crate::msgbus::{MessageBus, get_message_bus};
3901
3902        *get_message_bus().borrow_mut() = MessageBus::default();
3903
3904        pyo3::Python::initialize();
3905
3906        Python::attach(|py| {
3907            let py_actor = create_tracking_python_actor(py).unwrap();
3908
3909            let mut rust_actor = PyDataActor::new(None);
3910            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
3911            rust_actor.register(trader_id, clock, cache).unwrap();
3912            rust_actor.register_in_global_registries().unwrap();
3913            rust_actor.py_start().unwrap();
3914
3915            let bound_actor = Bound::new(py, rust_actor).unwrap();
3916            PyDataActor::py_subscribe_signal(&bound_actor, "", None).unwrap();
3917            let rust_actor = bound_actor.borrow_mut();
3918            let val1: Py<PyAny> = "1".into_py_any_unwrap(py);
3919            let val2: Py<PyAny> = "2".into_py_any_unwrap(py);
3920            let val3: Py<PyAny> = "3".into_py_any_unwrap(py);
3921            rust_actor.py_publish_signal(py, "alpha", val1, 0).unwrap();
3922            rust_actor.py_publish_signal(py, "beta", val2, 0).unwrap();
3923            rust_actor.py_publish_signal(py, "gamma", val3, 0).unwrap();
3924
3925            assert_eq!(python_method_call_count(&py_actor, py, "on_signal"), 3);
3926        });
3927    }
3928
3929    #[rstest]
3930    fn test_signal_customdata_unwraps_to_python_signal(
3931        clock: Rc<RefCell<VirtualClock>>,
3932        cache: Rc<RefCell<Cache>>,
3933        trader_id: TraderId,
3934    ) {
3935        // Exercises the `Signal::to_pyobject` path: a `CustomData` wrapping a
3936        // `Signal` reaches Python `on_data`, and the PyO3 `.data` getter must
3937        // successfully unwrap the inner `Arc<dyn CustomDataTrait>` into a
3938        // Python `Signal`. Without `Signal::to_pyobject`, the getter raises
3939        // `TypeError` and this assertion fails.
3940        use crate::msgbus::{MessageBus, get_message_bus};
3941
3942        *get_message_bus().borrow_mut() = MessageBus::default();
3943
3944        pyo3::Python::initialize();
3945
3946        Python::attach(|py| {
3947            let capture_code = c_str!(
3948                r#"
3949class CapturingActor:
3950    def __init__(self):
3951        self.captured = []
3952
3953    def on_start(self): pass
3954    def on_stop(self): pass
3955    def on_resume(self): pass
3956    def on_reset(self): pass
3957    def on_dispose(self): pass
3958    def on_degrade(self): pass
3959    def on_fault(self): pass
3960    def on_signal(self, signal): pass
3961
3962    def on_data(self, custom):
3963        # Exercise the CustomData.data getter: raises TypeError if the
3964        # inner payload cannot be converted back to a Python object.
3965        inner = custom.data
3966        self.captured.append((type(inner).__name__, inner.name, inner.value))
3967"#
3968            );
3969            py.run(capture_code, None, None).unwrap();
3970            let cls = py.eval(c_str!("CapturingActor"), None, None).unwrap();
3971            let py_actor: Py<PyAny> = cls.call0().unwrap().unbind();
3972
3973            let mut rust_actor = PyDataActor::new(None);
3974            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
3975            rust_actor.register(trader_id, clock, cache).unwrap();
3976            rust_actor.register_in_global_registries().unwrap();
3977            rust_actor.py_start().unwrap();
3978
3979            // Subscribe as custom-data for the signal's advertised DataType
3980            // (`data.SignalExample`) so `on_data` fires with the wrapping CustomData.
3981            let data_type = DataType::new("SignalExample", None, None);
3982            rust_actor
3983                .py_subscribe_data(py, data_type, None, None)
3984                .unwrap();
3985
3986            let val: Py<PyAny> = "1.5".into_py_any_unwrap(py);
3987            rust_actor.py_publish_signal(py, "example", val, 0).unwrap();
3988
3989            let captured = py_actor
3990                .bind(py)
3991                .getattr("captured")
3992                .unwrap()
3993                .extract::<Vec<(String, String, String)>>()
3994                .unwrap();
3995            assert_eq!(captured.len(), 1);
3996            assert_eq!(captured[0].0, "Signal");
3997            assert_eq!(captured[0].1, "example");
3998            assert_eq!(captured[0].2, "1.5");
3999        });
4000    }
4001
4002    #[rstest]
4003    fn test_add_and_update_synthetic_via_pyo3(
4004        clock: Rc<RefCell<VirtualClock>>,
4005        cache: Rc<RefCell<Cache>>,
4006        trader_id: TraderId,
4007    ) {
4008        use nautilus_model::{
4009            identifiers::{InstrumentId, Symbol},
4010            instruments::SyntheticInstrument,
4011        };
4012
4013        let actor = create_registered_actor(clock, cache.clone(), trader_id);
4014
4015        let comp1 = InstrumentId::from_str("BTC-USD.VENUE").unwrap();
4016        let comp2 = InstrumentId::from_str("ETH-USD.VENUE").unwrap();
4017        let formula = format!("({comp1} + {comp2}) / 2.0");
4018        let synthetic = SyntheticInstrument::builder()
4019            .symbol(Symbol::from("SYN"))
4020            .price_precision(2)
4021            .components(vec![comp1, comp2])
4022            .formula(&formula)
4023            .ts_event(UnixNanos::default())
4024            .ts_init(UnixNanos::default())
4025            .build()
4026            .unwrap();
4027        let synthetic_id = synthetic.id;
4028
4029        actor.py_add_synthetic(synthetic.clone()).unwrap();
4030        assert!(cache.borrow().synthetic(&synthetic_id).is_some());
4031
4032        // Adding again raises
4033        assert!(actor.py_add_synthetic(synthetic).is_err());
4034
4035        let new_formula = format!("{comp1} + {comp2}");
4036        let updated = SyntheticInstrument::builder()
4037            .symbol(Symbol::from("SYN"))
4038            .price_precision(2)
4039            .components(vec![comp1, comp2])
4040            .formula(&new_formula)
4041            .ts_event(UnixNanos::default())
4042            .ts_init(UnixNanos::default())
4043            .build()
4044            .unwrap();
4045        actor.py_update_synthetic(updated).unwrap();
4046        assert_eq!(
4047            cache.borrow().synthetic(&synthetic_id).unwrap().formula,
4048            new_formula
4049        );
4050
4051        // Updating a non-existent raises
4052        let missing = SyntheticInstrument::builder()
4053            .symbol(Symbol::from("GONE"))
4054            .price_precision(2)
4055            .components(vec![comp1, comp2])
4056            .formula(&formula)
4057            .ts_event(UnixNanos::default())
4058            .ts_init(UnixNanos::default())
4059            .build()
4060            .unwrap();
4061        assert!(actor.py_update_synthetic(missing).is_err());
4062    }
4063
4064    #[rstest]
4065    fn test_book_at_interval_invalid_interval_ms(
4066        clock: Rc<RefCell<VirtualClock>>,
4067        cache: Rc<RefCell<Cache>>,
4068        trader_id: TraderId,
4069        audusd_sim: CurrencyPair,
4070    ) {
4071        pyo3::Python::initialize();
4072
4073        let mut actor = create_registered_actor(clock, cache, trader_id);
4074
4075        pyo3::Python::attach(|py| {
4076            let result = actor.py_subscribe_book_at_interval(
4077                py,
4078                audusd_sim.id,
4079                BookType::L2_MBP,
4080                0,
4081                None,
4082                None,
4083                None,
4084            );
4085            assert!(result.is_err());
4086            assert_eq!(
4087                result.unwrap_err().to_string(),
4088                "ValueError: interval_ms must be > 0"
4089            );
4090
4091            let result = actor.py_unsubscribe_book_at_interval(py, audusd_sim.id, 0, None, None);
4092            assert!(result.is_err());
4093            assert_eq!(
4094                result.unwrap_err().to_string(),
4095                "ValueError: interval_ms must be > 0"
4096            );
4097        });
4098    }
4099
4100    #[rstest]
4101    #[case(None)]
4102    #[case(Some(25))]
4103    fn test_book_depth_subscription_methods_manage_handler(
4104        #[case] depth: Option<usize>,
4105        clock: Rc<RefCell<VirtualClock>>,
4106        cache: Rc<RefCell<Cache>>,
4107        trader_id: TraderId,
4108        audusd_sim: CurrencyPair,
4109    ) {
4110        pyo3::Python::initialize();
4111
4112        let mut actor = create_registered_actor(clock, cache, trader_id);
4113
4114        Python::attach(|py| {
4115            actor
4116                .py_subscribe_book_depth(
4117                    py,
4118                    audusd_sim.id,
4119                    BookType::L2_MBP,
4120                    depth,
4121                    None,
4122                    false,
4123                    None,
4124                )
4125                .unwrap();
4126            assert_eq!(actor.inner().depth_handler_count(), 1);
4127
4128            actor
4129                .py_unsubscribe_book_depth(py, audusd_sim.id, None, None)
4130                .unwrap();
4131            assert_eq!(actor.inner().depth_handler_count(), 0);
4132        });
4133    }
4134
4135    #[rstest]
4136    fn test_request_methods_signatures_exist() {
4137        let actor = create_unregistered_actor();
4138        assert!(actor.trader_id().is_none());
4139    }
4140
4141    #[rstest]
4142    fn test_data_actor_trait_implementation(
4143        clock: Rc<RefCell<VirtualClock>>,
4144        cache: Rc<RefCell<Cache>>,
4145        trader_id: TraderId,
4146    ) {
4147        let actor = create_registered_actor(clock, cache, trader_id);
4148        let state = actor.state();
4149        assert_eq!(state, ComponentState::Ready);
4150    }
4151
4152    #[rstest]
4153    fn test_python_reconnect_socket_enqueues_typed_command(
4154        clock: Rc<RefCell<VirtualClock>>,
4155        cache: Rc<RefCell<Cache>>,
4156        trader_id: TraderId,
4157    ) {
4158        let (system_tx, mut system_rx) = tokio::sync::mpsc::unbounded_channel();
4159        replace_system_command_sender(system_tx);
4160        let actor = create_registered_actor(clock, cache, trader_id);
4161
4162        actor
4163            .py_reconnect_socket(ClientId::from("POLYMARKET"), "polymarket-market-streams")
4164            .expect("valid reconnect command");
4165        let command = system_rx
4166            .try_recv()
4167            .expect("reconnect command should be queued");
4168        let SystemCommand::ReconnectSocket(command) = command;
4169
4170        assert_eq!(command.trader_id, trader_id);
4171        assert_eq!(command.client_id, ClientId::from("POLYMARKET"));
4172        assert_eq!(command.endpoint, "polymarket-market-streams");
4173        assert_eq!(command.ts_init, UnixNanos::default());
4174    }
4175
4176    #[rstest]
4177    fn test_python_reconnect_socket_returns_errors_without_panicking() {
4178        let actor = create_unregistered_actor();
4179
4180        assert!(
4181            actor
4182                .py_reconnect_socket(ClientId::from("POLYMARKET"), "polymarket-market-streams")
4183                .is_err()
4184        );
4185        assert!(
4186            actor
4187                .py_reconnect_socket(ClientId::from("POLYMARKET"), "wss://secret.example")
4188                .is_err()
4189        );
4190    }
4191
4192    fn sample_instrument() -> CurrencyPair {
4193        audusd_sim()
4194    }
4195
4196    fn sample_data() -> CustomData {
4197        stub_custom_data(1, 42, None, None)
4198    }
4199
4200    fn sample_time_event() -> TimeEvent {
4201        TimeEvent::new(
4202            Ustr::from("test_timer"),
4203            UUID4::new(),
4204            UnixNanos::default(),
4205            UnixNanos::default(),
4206        )
4207    }
4208
4209    fn sample_signal() -> Signal {
4210        Signal::new(
4211            Ustr::from("test_signal"),
4212            "1.0".to_string(),
4213            UnixNanos::default(),
4214            UnixNanos::default(),
4215        )
4216    }
4217
4218    fn sample_queue_state_changed(state: QueueState) -> QueueStateChanged {
4219        QueueStateChanged::new(
4220            TraderId::from("TRADER-001"),
4221            SystemChannel::ExecCommands,
4222            QueueCondition::Backlogged,
4223            state,
4224            17,
4225            23,
4226            UUID4::from("00000000-0000-4000-8000-000000000001"),
4227            UnixNanos::from(1_700_000_000_000_000_001),
4228            UnixNanos::from(1_700_000_000_000_000_002),
4229        )
4230    }
4231
4232    fn sample_socket_state_changed(state: SocketState) -> SocketStateChanged {
4233        SocketStateChanged::new(
4234            TraderId::from("TRADER-001"),
4235            ClientId::from("BINANCE"),
4236            Some(Venue::from("BINANCE")),
4237            Ustr::from("binance-futures-market-streams"),
4238            state,
4239            UUID4::from("00000000-0000-4000-8000-000000000001"),
4240            UnixNanos::from(1_700_000_000_000_000_001),
4241            UnixNanos::from(1_700_000_000_000_000_002),
4242        )
4243    }
4244
4245    fn sample_quote() -> QuoteTick {
4246        let instrument = sample_instrument();
4247        QuoteTick::new(
4248            instrument.id,
4249            Price::from("1.00000"),
4250            Price::from("1.00001"),
4251            Quantity::from(100_000),
4252            Quantity::from(100_000),
4253            UnixNanos::default(),
4254            UnixNanos::default(),
4255        )
4256    }
4257
4258    fn sample_trade() -> TradeTick {
4259        let instrument = sample_instrument();
4260        TradeTick::new(
4261            instrument.id,
4262            Price::from("1.00000"),
4263            Quantity::from(100_000),
4264            AggressorSide::Buy,
4265            TradeId::new("123456"),
4266            UnixNanos::default(),
4267            UnixNanos::default(),
4268        )
4269    }
4270
4271    fn sample_bar() -> Bar {
4272        let instrument = sample_instrument();
4273        let bar_type =
4274            BarType::from_str(&format!("{}-1-MINUTE-LAST-INTERNAL", instrument.id)).unwrap();
4275        Bar::new(
4276            bar_type,
4277            Price::from("1.00000"),
4278            Price::from("1.00010"),
4279            Price::from("0.99990"),
4280            Price::from("1.00005"),
4281            Quantity::from(100_000),
4282            UnixNanos::default(),
4283            UnixNanos::default(),
4284        )
4285    }
4286
4287    fn sample_book() -> OrderBook {
4288        OrderBook::new(sample_instrument().id, BookType::L2_MBP)
4289    }
4290
4291    fn sample_book_deltas() -> OrderBookDeltas {
4292        let instrument = sample_instrument();
4293        let delta =
4294            OrderBookDelta::clear(instrument.id, 0, UnixNanos::default(), UnixNanos::default());
4295        OrderBookDeltas::new(instrument.id, vec![delta])
4296    }
4297
4298    fn sample_book_depth() -> OrderBookDepth {
4299        stub_depth10()
4300    }
4301
4302    fn sample_mark_price() -> MarkPriceUpdate {
4303        MarkPriceUpdate::new(
4304            sample_instrument().id,
4305            Price::from("1.00000"),
4306            UnixNanos::default(),
4307            UnixNanos::default(),
4308        )
4309    }
4310
4311    fn sample_index_price() -> IndexPriceUpdate {
4312        IndexPriceUpdate::new(
4313            sample_instrument().id,
4314            Price::from("1.00000"),
4315            UnixNanos::default(),
4316            UnixNanos::default(),
4317        )
4318    }
4319
4320    fn sample_funding_rate() -> FundingRateUpdate {
4321        FundingRateUpdate::new(
4322            sample_instrument().id,
4323            "0.0001".parse().unwrap(),
4324            None,
4325            None,
4326            UnixNanos::default(),
4327            UnixNanos::default(),
4328        )
4329    }
4330
4331    fn sample_instrument_status() -> InstrumentStatus {
4332        InstrumentStatus::new(
4333            sample_instrument().id,
4334            MarketStatusAction::Trading,
4335            UnixNanos::default(),
4336            UnixNanos::default(),
4337            None,
4338            None,
4339            None,
4340            None,
4341            None,
4342        )
4343    }
4344
4345    fn sample_instrument_close() -> InstrumentClose {
4346        InstrumentClose::new(
4347            sample_instrument().id,
4348            Price::from("1.00000"),
4349            InstrumentCloseType::EndOfSession,
4350            UnixNanos::default(),
4351            UnixNanos::default(),
4352        )
4353    }
4354
4355    fn sample_option_greeks() -> OptionGreeks {
4356        OptionGreeks {
4357            instrument_id: sample_instrument().id,
4358            convention: GreeksConvention::BlackScholes,
4359            greeks: OptionGreekValues {
4360                delta: 0.55,
4361                gamma: 0.03,
4362                vega: 0.12,
4363                theta: -0.05,
4364                rho: 0.01,
4365            },
4366            mark_iv: Some(0.25),
4367            bid_iv: None,
4368            ask_iv: None,
4369            underlying_price: None,
4370            open_interest: None,
4371            ts_event: UnixNanos::default(),
4372            ts_init: UnixNanos::default(),
4373        }
4374    }
4375
4376    fn sample_option_chain() -> OptionChainSlice {
4377        OptionChainSlice {
4378            series_id: OptionSeriesId::new_derived(
4379                Venue::from("SIM"),
4380                Ustr::from("AUD"),
4381                Ustr::from("USD"),
4382                UnixNanos::from(1_711_036_800_000_000_000),
4383            ),
4384            atm_strike: None,
4385            calls: Default::default(),
4386            puts: Default::default(),
4387            ts_event: UnixNanos::default(),
4388            ts_init: UnixNanos::default(),
4389        }
4390    }
4391
4392    #[cfg(feature = "defi")]
4393    fn sample_block() -> Block {
4394        Block::new(
4395            "0x1234567890abcdef".to_string(),
4396            "0xabcdef1234567890".to_string(),
4397            12345,
4398            "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0".into(),
4399            21000,
4400            20000,
4401            UnixNanos::default(),
4402            Some(Blockchain::Ethereum),
4403        )
4404    }
4405
4406    #[cfg(feature = "defi")]
4407    fn sample_pool_components() -> (Arc<Chain>, Arc<Dex>, Pool) {
4408        let chain = Arc::new(Chain::new(Blockchain::Ethereum, 1));
4409        let dex = Arc::new(Dex::new(
4410            Chain::new(Blockchain::Ethereum, 1),
4411            DexType::UniswapV3,
4412            "0x1F98431c8aD98523631AE4a59f267346ea31F984",
4413            0,
4414            AmmType::CLAMM,
4415            "PoolCreated",
4416            "Swap",
4417            "Mint",
4418            "Burn",
4419            "Collect",
4420        ));
4421        let token0 = Token::new(
4422            chain.clone(),
4423            "0xa0b86a33e6441c8c06dd7b111a8c4e82e2b2a5e1"
4424                .parse()
4425                .unwrap(),
4426            "USDC".into(),
4427            "USD Coin".into(),
4428            6,
4429        );
4430        let token1 = Token::new(
4431            chain.clone(),
4432            "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
4433                .parse()
4434                .unwrap(),
4435            "WETH".into(),
4436            "Wrapped Ether".into(),
4437            18,
4438        );
4439        let pool_address = "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8"
4440            .parse()
4441            .unwrap();
4442        let pool_identifier: PoolIdentifier = "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8"
4443            .parse()
4444            .unwrap();
4445
4446        let pool = Pool::new(
4447            chain.clone(),
4448            dex.clone(),
4449            pool_address,
4450            pool_identifier,
4451            12345,
4452            token0,
4453            token1,
4454            Some(500),
4455            Some(10),
4456            UnixNanos::default(),
4457        );
4458
4459        (chain, dex, pool)
4460    }
4461
4462    #[cfg(feature = "defi")]
4463    fn sample_pool_swap() -> PoolSwap {
4464        let (chain, dex, pool) = sample_pool_components();
4465        PoolSwap::new(
4466            chain,
4467            dex,
4468            pool.instrument_id,
4469            pool.pool_identifier,
4470            12345,
4471            "0xabc123".to_string(),
4472            0,
4473            0,
4474            UnixNanos::default(),
4475            UnixNanos::default(),
4476            "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
4477                .parse()
4478                .unwrap(),
4479            "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
4480                .parse()
4481                .unwrap(),
4482            I256::from_str("1000000000000000000").unwrap(),
4483            I256::from_str("400000000000000").unwrap(),
4484            U160::from(59000000000000u128),
4485            1000000,
4486            100,
4487        )
4488    }
4489
4490    #[cfg(feature = "defi")]
4491    fn sample_pool_liquidity_update() -> PoolLiquidityUpdate {
4492        let (chain, dex, pool) = sample_pool_components();
4493        PoolLiquidityUpdate::new(
4494            chain,
4495            dex,
4496            pool.instrument_id,
4497            pool.pool_identifier,
4498            PoolLiquidityUpdateType::Mint,
4499            12345,
4500            "0xabc123".to_string(),
4501            0,
4502            0,
4503            Some(
4504                "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
4505                    .parse()
4506                    .unwrap(),
4507            ),
4508            "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
4509                .parse()
4510                .unwrap(),
4511            1000,
4512            U256::from(1_000u64),
4513            U256::from(2_000u64),
4514            -10,
4515            10,
4516            UnixNanos::default(),
4517            UnixNanos::default(),
4518        )
4519    }
4520
4521    #[cfg(feature = "defi")]
4522    fn sample_pool_fee_collect() -> PoolFeeCollect {
4523        let (chain, dex, pool) = sample_pool_components();
4524        PoolFeeCollect::new(
4525            chain,
4526            dex,
4527            pool.instrument_id,
4528            pool.pool_identifier,
4529            12345,
4530            "0xabc123".to_string(),
4531            0,
4532            0,
4533            "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
4534                .parse()
4535                .unwrap(),
4536            100,
4537            200,
4538            -10,
4539            10,
4540            UnixNanos::default(),
4541            UnixNanos::default(),
4542        )
4543    }
4544
4545    #[cfg(feature = "defi")]
4546    fn sample_pool_flash() -> PoolFlash {
4547        let (chain, dex, pool) = sample_pool_components();
4548        PoolFlash::new(
4549            chain,
4550            dex,
4551            pool.instrument_id,
4552            pool.pool_identifier,
4553            12345,
4554            "0xabc123".to_string(),
4555            0,
4556            0,
4557            UnixNanos::default(),
4558            UnixNanos::default(),
4559            "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
4560                .parse()
4561                .unwrap(),
4562            "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
4563                .parse()
4564                .unwrap(),
4565            U256::from(100u64),
4566            U256::from(200u64),
4567            U256::from(101u64),
4568            U256::from(201u64),
4569        )
4570    }
4571
4572    const TRACKING_ACTOR_CODE: &std::ffi::CStr = c_str!(
4573        r#"
4574class TrackingActor:
4575    """A mock Python actor that tracks all method calls."""
4576
4577    TRACKED_METHODS = {
4578        "on_start",
4579        "on_stop",
4580        "on_resume",
4581        "on_reset",
4582        "on_dispose",
4583        "on_degrade",
4584        "on_fault",
4585        "on_save",
4586        "on_load",
4587        "on_time_event",
4588        "on_data",
4589        "on_signal",
4590        "on_queue_state",
4591        "on_socket_state",
4592        "on_instrument",
4593        "on_quote",
4594        "on_trade",
4595        "on_bar",
4596        "on_book",
4597        "on_book_deltas",
4598        "on_book_depth",
4599        "on_mark_price",
4600        "on_index_price",
4601        "on_funding_rate",
4602        "on_instrument_status",
4603        "on_instrument_close",
4604        "on_option_greeks",
4605        "on_option_chain",
4606        "on_historical_data",
4607        "on_historical_book_deltas",
4608        "on_historical_book_depth",
4609        "on_historical_quotes",
4610        "on_historical_trades",
4611        "on_historical_funding_rates",
4612        "on_historical_bars",
4613        "on_historical_mark_prices",
4614        "on_historical_index_prices",
4615        "on_block",
4616        "on_pool",
4617        "on_pool_swap",
4618        "on_pool_liquidity_update",
4619        "on_pool_fee_collect",
4620        "on_pool_flash",
4621    }
4622
4623    def __init__(self):
4624        self.calls = []
4625        self.raises = False
4626
4627    def _record(self, method_name, *args):
4628        self.calls.append((method_name, args))
4629        if self.raises:
4630            raise RuntimeError("actor callback failure")
4631
4632    def was_called(self, method_name):
4633        return any(call[0] == method_name for call in self.calls)
4634
4635    def call_count(self, method_name):
4636        return sum(1 for call in self.calls if call[0] == method_name)
4637
4638    def last_call_args(self, method_name):
4639        for called_method, args in reversed(self.calls):
4640            if called_method == method_name:
4641                return args
4642        raise AssertionError(f"{method_name} was not called")
4643
4644    def last_loaded_state(self):
4645
4646        for method_name, args in reversed(self.calls):
4647            if method_name == "on_load":
4648                return args[0]
4649        return None
4650
4651    def on_save(self):
4652        self._record("on_save")
4653        return {"actor": b"saved"}
4654
4655    def on_load(self, state):
4656        self._record("on_load", dict(state))
4657
4658    def __getattr__(self, name):
4659        if name in self.TRACKED_METHODS:
4660            return lambda *args: self._record(name, *args)
4661        raise AttributeError(name)
4662"#
4663    );
4664
4665    fn create_tracking_python_actor(py: Python<'_>) -> PyResult<Py<PyAny>> {
4666        py.run(TRACKING_ACTOR_CODE, None, None)?;
4667        let tracking_actor_class = py.eval(c_str!("TrackingActor"), None, None)?;
4668        let instance = tracking_actor_class.call0()?;
4669        Ok(instance.unbind())
4670    }
4671
4672    fn python_method_was_called(py_actor: &Py<PyAny>, py: Python<'_>, method_name: &str) -> bool {
4673        py_actor
4674            .call_method1(py, "was_called", (method_name,))
4675            .and_then(|r| r.extract::<bool>(py))
4676            .unwrap_or(false)
4677    }
4678
4679    fn python_method_call_count(py_actor: &Py<PyAny>, py: Python<'_>, method_name: &str) -> i32 {
4680        py_actor
4681            .call_method1(py, "call_count", (method_name,))
4682            .and_then(|r| r.extract::<i32>(py))
4683            .unwrap_or(0)
4684    }
4685
4686    fn python_last_loaded_state(
4687        py_actor: &Py<PyAny>,
4688        py: Python<'_>,
4689    ) -> Option<HashMap<String, Vec<u8>>> {
4690        py_actor
4691            .call_method0(py, "last_loaded_state")
4692            .and_then(|result| result.extract::<Option<HashMap<String, Vec<u8>>>>(py))
4693            .unwrap_or(None)
4694    }
4695
4696    const TRACKING_INDICATOR_CODE: &std::ffi::CStr = c_str!(
4697        r#"
4698class TrackingIndicator:
4699    def __init__(self, events=None):
4700        self.initialized = False
4701        self.calls = []
4702        self.events = events
4703
4704    def handle_quote_tick(self, quote):
4705        self.calls.append("quote")
4706        if self.events is not None:
4707            self.events.append("indicator:quote")
4708
4709    def handle_trade_tick(self, trade):
4710        self.calls.append("trade")
4711        if self.events is not None:
4712            self.events.append("indicator:trade")
4713
4714    def handle_bar(self, bar):
4715        self.calls.append("bar")
4716        if self.events is not None:
4717            self.events.append("indicator:bar")
4718
4719    def call_count(self, name):
4720        return self.calls.count(name)
4721
4722class RaisingIndicator:
4723    initialized = True
4724
4725    def handle_quote_tick(self, quote):
4726        raise RuntimeError("indicator failed")
4727
4728class IndicatorEventActor:
4729    def __init__(self, events):
4730        self.events = events
4731
4732    def on_start(self):
4733        pass
4734
4735    def on_quote(self, quote):
4736        self.events.append("actor:quote")
4737
4738    def on_trade(self, trade):
4739        self.events.append("actor:trade")
4740
4741    def on_bar(self, bar):
4742        self.events.append("actor:bar")
4743"#
4744    );
4745
4746    fn create_tracking_python_indicator(py: Python<'_>) -> PyResult<Py<PyAny>> {
4747        py.run(TRACKING_INDICATOR_CODE, None, None)?;
4748        let indicator_class = py.eval(c_str!("TrackingIndicator"), None, None)?;
4749        Ok(indicator_class.call0()?.unbind())
4750    }
4751
4752    fn create_event_tracking_python_indicator(
4753        py: Python<'_>,
4754        events: &Bound<'_, PyList>,
4755    ) -> PyResult<Py<PyAny>> {
4756        py.run(TRACKING_INDICATOR_CODE, None, None)?;
4757        let indicator_class = py.eval(c_str!("TrackingIndicator"), None, None)?;
4758        Ok(indicator_class.call1((events,))?.unbind())
4759    }
4760
4761    fn create_raising_python_indicator(py: Python<'_>) -> PyResult<Py<PyAny>> {
4762        py.run(TRACKING_INDICATOR_CODE, None, None)?;
4763        let indicator_class = py.eval(c_str!("RaisingIndicator"), None, None)?;
4764        Ok(indicator_class.call0()?.unbind())
4765    }
4766
4767    fn create_indicator_event_actor(
4768        py: Python<'_>,
4769        events: &Bound<'_, PyList>,
4770    ) -> PyResult<Py<PyAny>> {
4771        py.run(TRACKING_INDICATOR_CODE, None, None)?;
4772        let actor_class = py.eval(c_str!("IndicatorEventActor"), None, None)?;
4773        Ok(actor_class.call1((events,))?.unbind())
4774    }
4775
4776    fn python_indicator_call_count(
4777        indicator: &Py<PyAny>,
4778        py: Python<'_>,
4779        method_name: &str,
4780    ) -> i32 {
4781        indicator
4782            .call_method1(py, "call_count", (method_name,))
4783            .and_then(|r| r.extract::<i32>(py))
4784            .unwrap_or(0)
4785    }
4786
4787    fn assert_python_dispatch<F>(
4788        py: Python<'_>,
4789        clock: Rc<RefCell<VirtualClock>>,
4790        cache: Rc<RefCell<Cache>>,
4791        trader_id: TraderId,
4792        method_name: &str,
4793        invoke: F,
4794    ) -> Py<PyAny>
4795    where
4796        F: FnOnce(&mut PyDataActor) -> anyhow::Result<()>,
4797    {
4798        let py_actor = create_tracking_python_actor(py).unwrap();
4799
4800        let mut rust_actor = PyDataActor::new(None);
4801        rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
4802        rust_actor.register(trader_id, clock, cache).unwrap();
4803
4804        let result = invoke(&mut rust_actor);
4805
4806        assert!(result.is_ok());
4807        assert!(python_method_was_called(&py_actor, py, method_name));
4808        assert_eq!(python_method_call_count(&py_actor, py, method_name), 1);
4809
4810        py_actor
4811    }
4812
4813    #[rstest]
4814    fn test_python_actor_callback_exception_preserves_traceback() {
4815        Python::initialize();
4816        Python::attach(|py| {
4817            let tracker = create_tracking_python_actor(py).unwrap();
4818            tracker.setattr(py, "raises", true).unwrap();
4819            let mut actor = PyDataActor::new(None);
4820            actor.set_python_instance(tracker.bind(py)).unwrap();
4821            let error = DataActor::on_bar(actor.inner_mut(), &sample_bar()).unwrap_err();
4822            let message = error.to_string();
4823
4824            assert_eq!(python_method_call_count(&tracker, py, "on_bar"), 1);
4825            assert!(message.contains("Python on_bar failed:"));
4826            assert!(message.contains("in _record"));
4827            assert!(message.contains("RuntimeError: actor callback failure"));
4828        });
4829    }
4830
4831    #[rstest]
4832    #[case("on_start")]
4833    #[case("on_stop")]
4834    #[case("on_resume")]
4835    #[case("on_reset")]
4836    #[case("on_dispose")]
4837    #[case("on_degrade")]
4838    #[case("on_fault")]
4839    fn test_python_dispatch_lifecycle_matrix(
4840        clock: Rc<RefCell<VirtualClock>>,
4841        cache: Rc<RefCell<Cache>>,
4842        trader_id: TraderId,
4843        #[case] method_name: &str,
4844    ) {
4845        pyo3::Python::initialize();
4846
4847        Python::attach(|py| {
4848            assert_python_dispatch(py, clock, cache, trader_id, method_name, |rust_actor| {
4849                match method_name {
4850                    "on_start" => DataActor::on_start(rust_actor.inner_mut()),
4851                    "on_stop" => DataActor::on_stop(rust_actor.inner_mut()),
4852                    "on_resume" => DataActor::on_resume(rust_actor.inner_mut()),
4853                    "on_reset" => DataActor::on_reset(rust_actor.inner_mut()),
4854                    "on_dispose" => DataActor::on_dispose(rust_actor.inner_mut()),
4855                    "on_degrade" => DataActor::on_degrade(rust_actor.inner_mut()),
4856                    "on_fault" => DataActor::on_fault(rust_actor.inner_mut()),
4857                    _ => unreachable!("unhandled lifecycle case: {method_name}"),
4858                }
4859            });
4860        });
4861    }
4862
4863    #[rstest]
4864    #[case("on_save")]
4865    #[case("on_load")]
4866    fn test_python_dispatch_persistence_matrix(
4867        clock: Rc<RefCell<VirtualClock>>,
4868        cache: Rc<RefCell<Cache>>,
4869        trader_id: TraderId,
4870        #[case] method_name: &str,
4871    ) {
4872        pyo3::Python::initialize();
4873
4874        Python::attach(|py| {
4875            assert_python_dispatch(py, clock, cache, trader_id, method_name, |rust_actor| {
4876                match method_name {
4877                    "on_save" => {
4878                        let state = DataActor::on_save(rust_actor.inner()).unwrap();
4879                        assert_eq!(
4880                            state.get("actor").map(Vec::as_slice),
4881                            Some(b"saved".as_slice())
4882                        );
4883                        Ok(())
4884                    }
4885                    "on_load" => {
4886                        let mut state = IndexMap::new();
4887                        state.insert("actor".to_string(), b"loaded".to_vec());
4888                        DataActor::on_load(rust_actor.inner_mut(), state)
4889                    }
4890                    _ => unreachable!("unhandled persistence case: {method_name}"),
4891                }
4892            });
4893        });
4894    }
4895
4896    #[rstest]
4897    fn test_python_persistence_methods_convert_state(
4898        clock: Rc<RefCell<VirtualClock>>,
4899        cache: Rc<RefCell<Cache>>,
4900        trader_id: TraderId,
4901    ) {
4902        pyo3::Python::initialize();
4903
4904        Python::attach(|py| {
4905            let py_actor = create_tracking_python_actor(py).unwrap();
4906
4907            let mut rust_actor = PyDataActor::new(None);
4908            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
4909            rust_actor.register(trader_id, clock, cache).unwrap();
4910
4911            let saved = rust_actor.py_save(py).unwrap();
4912            let saved_state = saved
4913                .bind(py)
4914                .extract::<HashMap<String, Vec<u8>>>()
4915                .unwrap();
4916            assert_eq!(
4917                saved_state.get("actor").map(Vec::as_slice),
4918                Some(&b"saved"[..])
4919            );
4920
4921            let load_state = PyDict::new(py);
4922            load_state
4923                .set_item("actor", PyBytes::new(py, b"loaded-from-python"))
4924                .unwrap();
4925
4926            rust_actor.py_load(&load_state).unwrap();
4927
4928            let loaded_state = python_last_loaded_state(&py_actor, py).unwrap();
4929            assert_eq!(
4930                loaded_state.get("actor").map(Vec::as_slice),
4931                Some(&b"loaded-from-python"[..])
4932            );
4933        });
4934    }
4935
4936    #[rstest]
4937    fn test_indicator_registration_exposes_readiness_and_registered_view(
4938        audusd_sim: CurrencyPair,
4939        bar_type: BarType,
4940    ) {
4941        pyo3::Python::initialize();
4942
4943        Python::attach(|py| {
4944            let mut rust_actor = PyDataActor::new(None);
4945            let indicator = create_tracking_python_indicator(py).unwrap();
4946
4947            assert_eq!(
4948                rust_actor
4949                    .py_registered_indicators(py)
4950                    .unwrap()
4951                    .bind(py)
4952                    .len()
4953                    .unwrap(),
4954                0
4955            );
4956            assert!(!rust_actor.py_indicators_initialized(py).unwrap());
4957
4958            rust_actor.py_register_indicator_for_quote_ticks(
4959                py,
4960                audusd_sim.id,
4961                indicator.clone_ref(py),
4962            );
4963            rust_actor.py_register_indicator_for_trade_ticks(
4964                py,
4965                audusd_sim.id,
4966                indicator.clone_ref(py),
4967            );
4968            rust_actor.py_register_indicator_for_bars(py, bar_type, indicator.clone_ref(py));
4969
4970            let registered = rust_actor.py_registered_indicators(py).unwrap();
4971            let registered = registered.bind(py);
4972
4973            assert_eq!(registered.len().unwrap(), 1);
4974            assert_eq!(
4975                registered.get_item(0).unwrap().as_ptr(),
4976                indicator.bind(py).as_ptr()
4977            );
4978            assert!(!rust_actor.py_indicators_initialized(py).unwrap());
4979
4980            indicator.bind(py).setattr("initialized", true).unwrap();
4981
4982            assert!(rust_actor.py_indicators_initialized(py).unwrap());
4983        });
4984    }
4985
4986    #[rstest]
4987    fn test_registered_indicators_receive_quote_trade_and_bar_before_actor_callbacks(
4988        clock: Rc<RefCell<VirtualClock>>,
4989        cache: Rc<RefCell<Cache>>,
4990        trader_id: TraderId,
4991    ) {
4992        pyo3::Python::initialize();
4993
4994        Python::attach(|py| {
4995            let events = PyList::empty(py);
4996            let py_actor = create_indicator_event_actor(py, &events).unwrap();
4997            let indicator = create_event_tracking_python_indicator(py, &events).unwrap();
4998
4999            let mut rust_actor = PyDataActor::new(None);
5000            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
5001            rust_actor.register(trader_id, clock, cache).unwrap();
5002            Component::start(rust_actor.inner_mut()).unwrap();
5003
5004            let quote = sample_quote();
5005            let trade = sample_trade();
5006            let bar = sample_bar();
5007            let external_bar_type = BarType::from_str(&format!(
5008                "{}-1-MINUTE-LAST-EXTERNAL",
5009                bar.bar_type.instrument_id()
5010            ))
5011            .unwrap();
5012
5013            rust_actor.py_register_indicator_for_quote_ticks(
5014                py,
5015                quote.instrument_id,
5016                indicator.clone_ref(py),
5017            );
5018            rust_actor.py_register_indicator_for_trade_ticks(
5019                py,
5020                trade.instrument_id,
5021                indicator.clone_ref(py),
5022            );
5023            rust_actor.py_register_indicator_for_bars(
5024                py,
5025                external_bar_type,
5026                indicator.clone_ref(py),
5027            );
5028
5029            DataActor::handle_quote(rust_actor.inner_mut(), &quote);
5030            DataActor::handle_trade(rust_actor.inner_mut(), &trade);
5031            DataActor::handle_bar(rust_actor.inner_mut(), &bar);
5032
5033            let events = events.extract::<Vec<String>>().unwrap();
5034
5035            assert_eq!(python_indicator_call_count(&indicator, py, "quote"), 1);
5036            assert_eq!(python_indicator_call_count(&indicator, py, "trade"), 1);
5037            assert_eq!(python_indicator_call_count(&indicator, py, "bar"), 1);
5038            assert_eq!(
5039                events,
5040                vec![
5041                    "indicator:quote",
5042                    "actor:quote",
5043                    "indicator:trade",
5044                    "actor:trade",
5045                    "indicator:bar",
5046                    "actor:bar",
5047                ]
5048            );
5049        });
5050    }
5051
5052    #[rstest]
5053    fn test_registered_indicators_receive_live_data_when_actor_not_running(
5054        clock: Rc<RefCell<VirtualClock>>,
5055        cache: Rc<RefCell<Cache>>,
5056        trader_id: TraderId,
5057    ) {
5058        pyo3::Python::initialize();
5059
5060        Python::attach(|py| {
5061            let events = PyList::empty(py);
5062            let py_actor = create_indicator_event_actor(py, &events).unwrap();
5063            let indicator = create_event_tracking_python_indicator(py, &events).unwrap();
5064
5065            let mut rust_actor = PyDataActor::new(None);
5066            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
5067            rust_actor.register(trader_id, clock, cache).unwrap();
5068
5069            let quote = sample_quote();
5070
5071            rust_actor.py_register_indicator_for_quote_ticks(
5072                py,
5073                quote.instrument_id,
5074                indicator.clone_ref(py),
5075            );
5076
5077            DataActor::handle_quote(rust_actor.inner_mut(), &quote);
5078
5079            let events = events.extract::<Vec<String>>().unwrap();
5080
5081            assert_eq!(python_indicator_call_count(&indicator, py, "quote"), 1);
5082            assert_eq!(events, vec!["indicator:quote"]);
5083        });
5084    }
5085
5086    #[rstest]
5087    fn test_registered_indicators_receive_historical_quote_trade_and_bar_batches() {
5088        pyo3::Python::initialize();
5089
5090        Python::attach(|py| {
5091            let mut rust_actor = PyDataActor::new(None);
5092            let indicator = create_tracking_python_indicator(py).unwrap();
5093            let quote = sample_quote();
5094            let trade = sample_trade();
5095            let bar = sample_bar();
5096            let quotes = vec![quote];
5097            let trades = vec![trade];
5098            let bars = vec![bar];
5099
5100            rust_actor.py_register_indicator_for_quote_ticks(
5101                py,
5102                quote.instrument_id,
5103                indicator.clone_ref(py),
5104            );
5105            rust_actor.py_register_indicator_for_trade_ticks(
5106                py,
5107                trade.instrument_id,
5108                indicator.clone_ref(py),
5109            );
5110            rust_actor.py_register_indicator_for_bars(py, bar.bar_type, indicator.clone_ref(py));
5111
5112            let client_id = ClientId::new("TEST");
5113            let quotes_response = QuotesResponse::new(
5114                UUID4::new(),
5115                client_id,
5116                quote.instrument_id,
5117                quotes,
5118                None,
5119                None,
5120                UnixNanos::default(),
5121                None,
5122            );
5123            let trades_response = TradesResponse::new(
5124                UUID4::new(),
5125                client_id,
5126                trade.instrument_id,
5127                trades,
5128                None,
5129                None,
5130                UnixNanos::default(),
5131                None,
5132            );
5133            let bars_response = BarsResponse::new(
5134                UUID4::new(),
5135                client_id,
5136                bar.bar_type,
5137                bars,
5138                None,
5139                None,
5140                UnixNanos::default(),
5141                None,
5142            );
5143
5144            DataActor::handle_quotes_response(rust_actor.inner_mut(), &quotes_response);
5145            DataActor::handle_trades_response(rust_actor.inner_mut(), &trades_response);
5146            DataActor::handle_bars_response(rust_actor.inner_mut(), &bars_response);
5147
5148            assert_eq!(python_indicator_call_count(&indicator, py, "quote"), 1);
5149            assert_eq!(python_indicator_call_count(&indicator, py, "trade"), 1);
5150            assert_eq!(python_indicator_call_count(&indicator, py, "bar"), 1);
5151        });
5152    }
5153
5154    #[rstest]
5155    fn test_indicators_initialized_requires_all_registered_indicators(audusd_sim: CurrencyPair) {
5156        pyo3::Python::initialize();
5157
5158        Python::attach(|py| {
5159            let mut rust_actor = PyDataActor::new(None);
5160            let first = create_tracking_python_indicator(py).unwrap();
5161            let second = create_tracking_python_indicator(py).unwrap();
5162
5163            rust_actor.py_register_indicator_for_quote_ticks(
5164                py,
5165                audusd_sim.id,
5166                first.clone_ref(py),
5167            );
5168            rust_actor.py_register_indicator_for_quote_ticks(
5169                py,
5170                audusd_sim.id,
5171                second.clone_ref(py),
5172            );
5173
5174            first.bind(py).setattr("initialized", true).unwrap();
5175
5176            assert!(!rust_actor.py_indicators_initialized(py).unwrap());
5177
5178            second.bind(py).setattr("initialized", true).unwrap();
5179
5180            assert!(rust_actor.py_indicators_initialized(py).unwrap());
5181        });
5182    }
5183
5184    #[rstest]
5185    fn test_duplicate_indicator_registration_does_not_duplicate_callbacks(
5186        clock: Rc<RefCell<VirtualClock>>,
5187        cache: Rc<RefCell<Cache>>,
5188        trader_id: TraderId,
5189    ) {
5190        pyo3::Python::initialize();
5191
5192        Python::attach(|py| {
5193            let mut rust_actor = PyDataActor::new(None);
5194            let indicator = create_tracking_python_indicator(py).unwrap();
5195            let quote = sample_quote();
5196            let trade = sample_trade();
5197            let bar = sample_bar();
5198            rust_actor.register(trader_id, clock, cache).unwrap();
5199            Component::start(rust_actor.inner_mut()).unwrap();
5200
5201            rust_actor.py_register_indicator_for_quote_ticks(
5202                py,
5203                quote.instrument_id,
5204                indicator.clone_ref(py),
5205            );
5206            rust_actor.py_register_indicator_for_quote_ticks(
5207                py,
5208                quote.instrument_id,
5209                indicator.clone_ref(py),
5210            );
5211            rust_actor.py_register_indicator_for_trade_ticks(
5212                py,
5213                trade.instrument_id,
5214                indicator.clone_ref(py),
5215            );
5216            rust_actor.py_register_indicator_for_trade_ticks(
5217                py,
5218                trade.instrument_id,
5219                indicator.clone_ref(py),
5220            );
5221            rust_actor.py_register_indicator_for_bars(py, bar.bar_type, indicator.clone_ref(py));
5222            rust_actor.py_register_indicator_for_bars(py, bar.bar_type, indicator.clone_ref(py));
5223
5224            DataActor::handle_quote(rust_actor.inner_mut(), &quote);
5225            DataActor::handle_trade(rust_actor.inner_mut(), &trade);
5226            DataActor::handle_bar(rust_actor.inner_mut(), &bar);
5227
5228            assert_eq!(
5229                rust_actor
5230                    .py_registered_indicators(py)
5231                    .unwrap()
5232                    .bind(py)
5233                    .len()
5234                    .unwrap(),
5235                1
5236            );
5237            assert_eq!(python_indicator_call_count(&indicator, py, "quote"), 1);
5238            assert_eq!(python_indicator_call_count(&indicator, py, "trade"), 1);
5239            assert_eq!(python_indicator_call_count(&indicator, py, "bar"), 1);
5240        });
5241    }
5242
5243    #[rstest]
5244    fn test_indicator_error_prevents_actor_callback(
5245        clock: Rc<RefCell<VirtualClock>>,
5246        cache: Rc<RefCell<Cache>>,
5247        trader_id: TraderId,
5248    ) {
5249        pyo3::Python::initialize();
5250
5251        Python::attach(|py| {
5252            let events = PyList::empty(py);
5253            let py_actor = create_indicator_event_actor(py, &events).unwrap();
5254            let indicator = create_raising_python_indicator(py).unwrap();
5255
5256            let mut rust_actor = PyDataActor::new(None);
5257            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
5258            rust_actor.register(trader_id, clock, cache).unwrap();
5259            Component::start(rust_actor.inner_mut()).unwrap();
5260
5261            let quote = sample_quote();
5262
5263            rust_actor.py_register_indicator_for_quote_ticks(py, quote.instrument_id, indicator);
5264
5265            DataActor::handle_quote(rust_actor.inner_mut(), &quote);
5266            let events = events.extract::<Vec<String>>().unwrap();
5267
5268            assert!(events.is_empty());
5269        });
5270    }
5271
5272    #[rstest]
5273    #[case("on_time_event")]
5274    #[case("on_data")]
5275    #[case("on_signal")]
5276    #[case("on_queue_state")]
5277    #[case("on_socket_state")]
5278    #[case("on_instrument")]
5279    #[case("on_quote")]
5280    #[case("on_trade")]
5281    #[case("on_bar")]
5282    #[case("on_book")]
5283    #[case("on_book_deltas")]
5284    #[case("on_book_depth")]
5285    #[case("on_mark_price")]
5286    #[case("on_index_price")]
5287    #[case("on_funding_rate")]
5288    #[case("on_instrument_status")]
5289    #[case("on_instrument_close")]
5290    #[case("on_option_greeks")]
5291    #[case("on_option_chain")]
5292    fn test_python_dispatch_typed_callback_matrix(
5293        clock: Rc<RefCell<VirtualClock>>,
5294        cache: Rc<RefCell<Cache>>,
5295        trader_id: TraderId,
5296        #[case] method_name: &str,
5297    ) {
5298        pyo3::Python::initialize();
5299
5300        Python::attach(|py| {
5301            assert_python_dispatch(py, clock, cache, trader_id, method_name, |rust_actor| {
5302                match method_name {
5303                    "on_time_event" => {
5304                        let event = sample_time_event();
5305                        rust_actor.inner_mut().on_time_event(&event)
5306                    }
5307                    "on_data" => {
5308                        let data = sample_data();
5309                        rust_actor.inner_mut().on_data(&data)
5310                    }
5311                    "on_signal" => {
5312                        let signal = sample_signal();
5313                        rust_actor.inner_mut().on_signal(&signal)
5314                    }
5315                    "on_queue_state" => {
5316                        let event = sample_queue_state_changed(QueueState::Triggered);
5317                        rust_actor.inner_mut().on_queue_state(&event)
5318                    }
5319                    "on_socket_state" => {
5320                        let event = sample_socket_state_changed(SocketState::Connected);
5321                        rust_actor.inner_mut().on_socket_state(&event)
5322                    }
5323                    "on_instrument" => {
5324                        let instrument = InstrumentAny::CurrencyPair(sample_instrument());
5325                        rust_actor.inner_mut().on_instrument(&instrument)
5326                    }
5327                    "on_quote" => {
5328                        let quote = sample_quote();
5329                        rust_actor.inner_mut().on_quote(&quote)
5330                    }
5331                    "on_trade" => {
5332                        let trade = sample_trade();
5333                        rust_actor.inner_mut().on_trade(&trade)
5334                    }
5335                    "on_bar" => {
5336                        let bar = sample_bar();
5337                        rust_actor.inner_mut().on_bar(&bar)
5338                    }
5339                    "on_book" => {
5340                        let book = sample_book();
5341                        rust_actor.inner_mut().on_book(&book)
5342                    }
5343                    "on_book_deltas" => {
5344                        let deltas = sample_book_deltas();
5345                        rust_actor.inner_mut().on_book_deltas(&deltas)
5346                    }
5347                    "on_book_depth" => {
5348                        let depth = sample_book_depth();
5349                        rust_actor.inner_mut().on_book_depth(&depth)
5350                    }
5351                    "on_mark_price" => {
5352                        let update = sample_mark_price();
5353                        rust_actor.inner_mut().on_mark_price(&update)
5354                    }
5355                    "on_index_price" => {
5356                        let update = sample_index_price();
5357                        rust_actor.inner_mut().on_index_price(&update)
5358                    }
5359                    "on_funding_rate" => {
5360                        let update = sample_funding_rate();
5361                        rust_actor.inner_mut().on_funding_rate(&update)
5362                    }
5363                    "on_instrument_status" => {
5364                        let status = sample_instrument_status();
5365                        rust_actor.inner_mut().on_instrument_status(&status)
5366                    }
5367                    "on_instrument_close" => {
5368                        let close = sample_instrument_close();
5369                        rust_actor.inner_mut().on_instrument_close(&close)
5370                    }
5371                    "on_option_greeks" => {
5372                        let greeks = sample_option_greeks();
5373                        rust_actor.inner_mut().on_option_greeks(&greeks)
5374                    }
5375                    "on_option_chain" => {
5376                        let chain = sample_option_chain();
5377                        rust_actor.inner_mut().on_option_chain(&chain)
5378                    }
5379                    _ => unreachable!("unhandled typed callback case: {method_name}"),
5380                }
5381            });
5382        });
5383    }
5384
5385    #[rstest]
5386    #[case("on_historical_data")]
5387    #[case("on_historical_book_deltas")]
5388    #[case("on_historical_book_depth")]
5389    #[case("on_historical_quotes")]
5390    #[case("on_historical_trades")]
5391    #[case("on_historical_funding_rates")]
5392    #[case("on_historical_bars")]
5393    #[case("on_historical_mark_prices")]
5394    #[case("on_historical_index_prices")]
5395    fn test_python_dispatch_historical_callback_matrix(
5396        clock: Rc<RefCell<VirtualClock>>,
5397        cache: Rc<RefCell<Cache>>,
5398        trader_id: TraderId,
5399        #[case] method_name: &str,
5400    ) {
5401        pyo3::Python::initialize();
5402
5403        Python::attach(|py| {
5404            assert_python_dispatch(py, clock, cache, trader_id, method_name, |rust_actor| {
5405                match method_name {
5406                    "on_historical_data" => {
5407                        let data = sample_data();
5408                        rust_actor.inner_mut().on_historical_data(&data)
5409                    }
5410                    "on_historical_book_deltas" => {
5411                        let deltas = sample_book_deltas().deltas;
5412                        rust_actor.inner_mut().on_historical_book_deltas(&deltas)
5413                    }
5414                    "on_historical_book_depth" => {
5415                        let depths = vec![sample_book_depth()];
5416                        rust_actor.inner_mut().on_historical_book_depth(&depths)
5417                    }
5418                    "on_historical_quotes" => {
5419                        let quotes = vec![sample_quote()];
5420                        rust_actor.inner_mut().on_historical_quotes(&quotes)
5421                    }
5422                    "on_historical_trades" => {
5423                        let trades = vec![sample_trade()];
5424                        rust_actor.inner_mut().on_historical_trades(&trades)
5425                    }
5426                    "on_historical_funding_rates" => {
5427                        let funding_rates = vec![sample_funding_rate()];
5428                        rust_actor
5429                            .inner_mut()
5430                            .on_historical_funding_rates(&funding_rates)
5431                    }
5432                    "on_historical_bars" => {
5433                        let bars = vec![sample_bar()];
5434                        rust_actor.inner_mut().on_historical_bars(&bars)
5435                    }
5436                    "on_historical_mark_prices" => {
5437                        let mark_prices = vec![sample_mark_price()];
5438                        rust_actor
5439                            .inner_mut()
5440                            .on_historical_mark_prices(&mark_prices)
5441                    }
5442                    "on_historical_index_prices" => {
5443                        let index_prices = vec![sample_index_price()];
5444                        rust_actor
5445                            .inner_mut()
5446                            .on_historical_index_prices(&index_prices)
5447                    }
5448                    _ => unreachable!("unhandled historical callback case: {method_name}"),
5449                }
5450            });
5451        });
5452    }
5453
5454    #[rstest]
5455    fn test_python_dispatch_historical_book_deltas_preserves_batch(
5456        clock: Rc<RefCell<VirtualClock>>,
5457        cache: Rc<RefCell<Cache>>,
5458        trader_id: TraderId,
5459    ) {
5460        pyo3::Python::initialize();
5461
5462        Python::attach(|py| {
5463            let expected = stub_deltas().deltas;
5464            let py_actor = assert_python_dispatch(
5465                py,
5466                clock,
5467                cache,
5468                trader_id,
5469                "on_historical_book_deltas",
5470                |rust_actor| rust_actor.inner_mut().on_historical_book_deltas(&expected),
5471            );
5472            let actual = py_actor
5473                .call_method1(py, "last_call_args", ("on_historical_book_deltas",))
5474                .unwrap()
5475                .bind(py)
5476                .get_item(0)
5477                .unwrap()
5478                .extract::<Vec<OrderBookDelta>>()
5479                .unwrap();
5480
5481            assert_eq!(actual, expected);
5482        });
5483    }
5484
5485    #[rstest]
5486    fn test_python_dispatch_historical_book_depth_preserves_batch(
5487        clock: Rc<RefCell<VirtualClock>>,
5488        cache: Rc<RefCell<Cache>>,
5489        trader_id: TraderId,
5490    ) {
5491        pyo3::Python::initialize();
5492
5493        Python::attach(|py| {
5494            let first = stub_depth10();
5495            let mut second = first.clone();
5496            second.sequence = 17;
5497            second.ts_event = UnixNanos::from(18);
5498            second.ts_init = UnixNanos::from(19);
5499            let expected = vec![first, second];
5500            let py_actor = assert_python_dispatch(
5501                py,
5502                clock,
5503                cache,
5504                trader_id,
5505                "on_historical_book_depth",
5506                |rust_actor| rust_actor.inner_mut().on_historical_book_depth(&expected),
5507            );
5508            let actual = py_actor
5509                .call_method1(py, "last_call_args", ("on_historical_book_depth",))
5510                .unwrap()
5511                .bind(py)
5512                .get_item(0)
5513                .unwrap()
5514                .extract::<Vec<OrderBookDepth>>()
5515                .unwrap();
5516
5517            assert_eq!(actual, expected);
5518        });
5519    }
5520
5521    #[cfg(feature = "defi")]
5522    #[rstest]
5523    #[case("on_block")]
5524    #[case("on_pool")]
5525    #[case("on_pool_swap")]
5526    #[case("on_pool_liquidity_update")]
5527    #[case("on_pool_fee_collect")]
5528    #[case("on_pool_flash")]
5529    fn test_python_dispatch_defi_callback_matrix(
5530        clock: Rc<RefCell<VirtualClock>>,
5531        cache: Rc<RefCell<Cache>>,
5532        trader_id: TraderId,
5533        #[case] method_name: &str,
5534    ) {
5535        pyo3::Python::initialize();
5536
5537        Python::attach(|py| {
5538            assert_python_dispatch(py, clock, cache, trader_id, method_name, |rust_actor| {
5539                match method_name {
5540                    "on_block" => {
5541                        let block = sample_block();
5542                        rust_actor.inner_mut().on_block(&block)
5543                    }
5544                    "on_pool" => {
5545                        let (_chain, _dex, pool) = sample_pool_components();
5546                        rust_actor.inner_mut().on_pool(&pool)
5547                    }
5548                    "on_pool_swap" => {
5549                        let swap = sample_pool_swap();
5550                        rust_actor.inner_mut().on_pool_swap(&swap)
5551                    }
5552                    "on_pool_liquidity_update" => {
5553                        let update = sample_pool_liquidity_update();
5554                        rust_actor.inner_mut().on_pool_liquidity_update(&update)
5555                    }
5556                    "on_pool_fee_collect" => {
5557                        let collect = sample_pool_fee_collect();
5558                        rust_actor.inner_mut().on_pool_fee_collect(&collect)
5559                    }
5560                    "on_pool_flash" => {
5561                        let flash = sample_pool_flash();
5562                        rust_actor.inner_mut().on_pool_flash(&flash)
5563                    }
5564                    _ => unreachable!("unhandled defi callback case: {method_name}"),
5565                }
5566            });
5567        });
5568    }
5569
5570    #[rstest]
5571    fn test_python_dispatch_multiple_calls_tracked(
5572        clock: Rc<RefCell<VirtualClock>>,
5573        cache: Rc<RefCell<Cache>>,
5574        trader_id: TraderId,
5575        audusd_sim: CurrencyPair,
5576    ) {
5577        pyo3::Python::initialize();
5578
5579        Python::attach(|py| {
5580            let py_actor = create_tracking_python_actor(py).unwrap();
5581
5582            let mut rust_actor = PyDataActor::new(None);
5583            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
5584            rust_actor.register(trader_id, clock, cache).unwrap();
5585
5586            let quote = QuoteTick::new(
5587                audusd_sim.id,
5588                Price::from("1.00000"),
5589                Price::from("1.00001"),
5590                Quantity::from(100_000),
5591                Quantity::from(100_000),
5592                UnixNanos::default(),
5593                UnixNanos::default(),
5594            );
5595
5596            rust_actor.inner_mut().on_quote(&quote).unwrap();
5597            rust_actor.inner_mut().on_quote(&quote).unwrap();
5598            rust_actor.inner_mut().on_quote(&quote).unwrap();
5599
5600            assert_eq!(python_method_call_count(&py_actor, py, "on_quote"), 3);
5601        });
5602    }
5603
5604    #[rstest]
5605    fn test_python_dispatch_historical_custom_data_preserves_payload_shape(
5606        clock: Rc<RefCell<VirtualClock>>,
5607        cache: Rc<RefCell<Cache>>,
5608        trader_id: TraderId,
5609        client_id: ClientId,
5610    ) {
5611        pyo3::Python::initialize();
5612
5613        Python::attach(|py| {
5614            let py_actor = create_tracking_python_actor(py).unwrap();
5615            let mut rust_actor = PyDataActor::new(None);
5616            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
5617            rust_actor.register(trader_id, clock, cache).unwrap();
5618
5619            let data = vec![
5620                stub_custom_data(1, 42, None, None),
5621                stub_custom_data(2, 84, None, None),
5622            ];
5623            let scalar = stub_custom_data(3, 126, None, None);
5624            let scalar_response = CustomDataResponse::new(
5625                UUID4::new(),
5626                client_id,
5627                None,
5628                scalar.data_type.clone(),
5629                scalar.clone(),
5630                None,
5631                None,
5632                UnixNanos::default(),
5633                None,
5634            );
5635
5636            DataActor::handle_data_response(rust_actor.inner_mut(), &scalar_response);
5637
5638            let actual_scalar = py_actor
5639                .call_method1(py, "last_call_args", ("on_historical_data",))
5640                .unwrap()
5641                .bind(py)
5642                .get_item(0)
5643                .unwrap()
5644                .extract::<CustomData>()
5645                .unwrap();
5646
5647            assert_eq!(
5648                python_method_call_count(&py_actor, py, "on_historical_data"),
5649                1
5650            );
5651            assert_eq!(actual_scalar, scalar);
5652
5653            let empty_response = CustomDataResponse::new(
5654                UUID4::new(),
5655                client_id,
5656                None,
5657                data[0].data_type.clone(),
5658                Vec::<CustomData>::new(),
5659                None,
5660                None,
5661                UnixNanos::default(),
5662                None,
5663            );
5664
5665            DataActor::handle_data_response(rust_actor.inner_mut(), &empty_response);
5666
5667            let empty = py_actor
5668                .call_method1(py, "last_call_args", ("on_historical_data",))
5669                .unwrap()
5670                .bind(py)
5671                .get_item(0)
5672                .unwrap()
5673                .extract::<Vec<CustomData>>()
5674                .unwrap();
5675
5676            assert_eq!(
5677                python_method_call_count(&py_actor, py, "on_historical_data"),
5678                2
5679            );
5680            assert!(empty.is_empty());
5681
5682            let response = CustomDataResponse::new(
5683                UUID4::new(),
5684                client_id,
5685                None,
5686                data[0].data_type.clone(),
5687                data.clone(),
5688                None,
5689                None,
5690                UnixNanos::default(),
5691                None,
5692            );
5693
5694            DataActor::handle_data_response(rust_actor.inner_mut(), &response);
5695
5696            let actual = py_actor
5697                .call_method1(py, "last_call_args", ("on_historical_data",))
5698                .unwrap()
5699                .bind(py)
5700                .get_item(0)
5701                .unwrap()
5702                .extract::<Vec<CustomData>>()
5703                .unwrap();
5704
5705            assert_eq!(
5706                python_method_call_count(&py_actor, py, "on_historical_data"),
5707                3
5708            );
5709            assert_eq!(actual, data);
5710        });
5711    }
5712
5713    #[rstest]
5714    fn test_python_dispatch_no_call_when_py_self_not_set(
5715        clock: Rc<RefCell<VirtualClock>>,
5716        cache: Rc<RefCell<Cache>>,
5717        trader_id: TraderId,
5718    ) {
5719        pyo3::Python::initialize();
5720
5721        Python::attach(|_py| {
5722            let mut rust_actor = PyDataActor::new(None);
5723            rust_actor.register(trader_id, clock, cache).unwrap();
5724
5725            // When py_self is None, the dispatch returns Ok(()) without calling Python
5726            let result = DataActor::on_start(rust_actor.inner_mut());
5727            assert!(result.is_ok());
5728        });
5729    }
5730
5731    #[rstest]
5732    fn test_python_on_historical_data_rejects_non_custom_data(
5733        clock: Rc<RefCell<VirtualClock>>,
5734        cache: Rc<RefCell<Cache>>,
5735        trader_id: TraderId,
5736    ) {
5737        pyo3::Python::initialize();
5738
5739        let mut rust_actor = PyDataActor::new(None);
5740        rust_actor.register(trader_id, clock, cache).unwrap();
5741
5742        let non_custom: String = "not CustomData".to_string();
5743        let result = rust_actor.inner_mut().on_historical_data(&non_custom);
5744
5745        assert!(result.is_err());
5746        assert!(result.unwrap_err().to_string().contains("unsupported type"));
5747    }
5748
5749    #[rstest]
5750    fn test_python_self_is_weak() {
5751        Python::initialize();
5752
5753        Python::attach(|py| {
5754            let instance = py
5755                .get_type::<PyDataActor>()
5756                .call0()
5757                .expect("DataActor should construct");
5758            let weakref =
5759                PyWeakrefReference::new(&instance).expect("DataActor should be weak-referenceable");
5760            assert!(weakref.upgrade().is_some());
5761
5762            drop(instance);
5763
5764            // A strong `py_self` would form an untraceable Rust-Python cycle and keep this alive
5765            assert!(
5766                weakref.upgrade().is_none(),
5767                "an unregistered DataActor must be collected once its last Python owner is dropped",
5768            );
5769        });
5770    }
5771
5772    #[rstest::rstest]
5773    #[case("on_start")]
5774    #[case("on_stop")]
5775    #[case("on_resume")]
5776    #[case("on_reset")]
5777    #[case("on_dispose")]
5778    #[case("on_degrade")]
5779    #[case("on_fault")]
5780    #[case("on_save")]
5781    #[case("on_load")]
5782    #[case("on_time_event")]
5783    #[case("on_data")]
5784    #[case("on_signal")]
5785    #[case("on_queue_state")]
5786    #[case("on_socket_state")]
5787    #[case("on_instrument")]
5788    #[case("on_quote")]
5789    #[case("on_trade")]
5790    #[case("on_bar")]
5791    #[case("on_book_deltas")]
5792    #[case("on_book_depth")]
5793    #[case("on_book")]
5794    #[case("on_mark_price")]
5795    #[case("on_index_price")]
5796    #[case("on_funding_rate")]
5797    #[case("on_instrument_status")]
5798    #[case("on_instrument_close")]
5799    #[case("on_option_greeks")]
5800    #[case("on_option_chain")]
5801    #[case("on_historical_data")]
5802    #[case("on_historical_book_deltas")]
5803    #[case("on_historical_book_depth")]
5804    #[case("on_historical_quotes")]
5805    #[case("on_historical_trades")]
5806    #[case("on_historical_funding_rates")]
5807    #[case("on_historical_bars")]
5808    #[case("on_historical_mark_prices")]
5809    #[case("on_historical_index_prices")]
5810    fn test_default_callback_during_exclusive_borrow(#[case] method: &str) {
5811        Python::initialize();
5812        Python::attach(|py| {
5813            let argument = match method {
5814                "on_start" | "on_stop" | "on_resume" | "on_reset" | "on_dispose" | "on_degrade"
5815                | "on_fault" | "on_save" => None,
5816                "on_load" => Some(PyDict::new(py).into_any().unbind()),
5817                "on_time_event" => Some(sample_time_event().into_py_any(py).unwrap()),
5818                "on_data" | "on_instrument" | "on_historical_data" => Some(py.None()),
5819                "on_signal" => Some(sample_signal().into_py_any(py).unwrap()),
5820                "on_queue_state" => Some(
5821                    sample_queue_state_changed(QueueState::Triggered)
5822                        .into_py_any(py)
5823                        .unwrap(),
5824                ),
5825                "on_socket_state" => Some(
5826                    sample_socket_state_changed(SocketState::Connected)
5827                        .into_py_any(py)
5828                        .unwrap(),
5829                ),
5830                "on_quote" => Some(sample_quote().into_py_any(py).unwrap()),
5831                "on_trade" => Some(sample_trade().into_py_any(py).unwrap()),
5832                "on_bar" => Some(sample_bar().into_py_any(py).unwrap()),
5833                "on_book_deltas" => Some(sample_book_deltas().into_py_any(py).unwrap()),
5834                "on_book_depth" => Some(sample_book_depth().into_py_any(py).unwrap()),
5835                "on_book" => Some(sample_book().into_py_any(py).unwrap()),
5836                "on_mark_price" => Some(sample_mark_price().into_py_any(py).unwrap()),
5837                "on_index_price" => Some(sample_index_price().into_py_any(py).unwrap()),
5838                "on_funding_rate" => Some(sample_funding_rate().into_py_any(py).unwrap()),
5839                "on_instrument_status" => Some(sample_instrument_status().into_py_any(py).unwrap()),
5840                "on_instrument_close" => Some(sample_instrument_close().into_py_any(py).unwrap()),
5841                "on_option_greeks" => Some(sample_option_greeks().into_py_any(py).unwrap()),
5842                "on_option_chain" => Some(sample_option_chain().into_py_any(py).unwrap()),
5843                "on_historical_book_deltas"
5844                | "on_historical_book_depth"
5845                | "on_historical_quotes"
5846                | "on_historical_trades"
5847                | "on_historical_funding_rates"
5848                | "on_historical_bars"
5849                | "on_historical_mark_prices"
5850                | "on_historical_index_prices" => Some(PyList::empty(py).into_any().unbind()),
5851                _ => unreachable!(),
5852            };
5853
5854            let instance = py.get_type::<PyDataActor>().call0().unwrap();
5855            let _borrow = instance
5856                .extract::<pyo3::PyRefMut<'_, PyDataActor>>()
5857                .unwrap();
5858
5859            let result = match argument {
5860                Some(argument) => instance.call_method1(method, (argument,)),
5861                None => instance.call_method0(method),
5862            }
5863            .unwrap();
5864
5865            if method == "on_save" {
5866                assert_eq!(result.len().unwrap(), 0);
5867            } else {
5868                assert!(result.is_none());
5869            }
5870        });
5871    }
5872
5873    #[cfg(feature = "defi")]
5874    #[rstest::rstest]
5875    #[case("on_block")]
5876    #[case("on_pool")]
5877    #[case("on_pool_swap")]
5878    #[case("on_pool_liquidity_update")]
5879    #[case("on_pool_fee_collect")]
5880    #[case("on_pool_flash")]
5881    fn test_default_defi_callback_during_exclusive_borrow(#[case] method: &str) {
5882        Python::initialize();
5883        Python::attach(|py| {
5884            let argument = match method {
5885                "on_block" => sample_block().into_py_any(py).unwrap(),
5886                "on_pool" => sample_pool_components().2.into_py_any(py).unwrap(),
5887                "on_pool_swap" => sample_pool_swap().into_py_any(py).unwrap(),
5888                "on_pool_liquidity_update" => {
5889                    sample_pool_liquidity_update().into_py_any(py).unwrap()
5890                }
5891                "on_pool_fee_collect" => sample_pool_fee_collect().into_py_any(py).unwrap(),
5892                "on_pool_flash" => sample_pool_flash().into_py_any(py).unwrap(),
5893                _ => unreachable!(),
5894            };
5895
5896            let instance = py.get_type::<PyDataActor>().call0().unwrap();
5897            let _borrow = instance
5898                .extract::<pyo3::PyRefMut<'_, PyDataActor>>()
5899                .unwrap();
5900            assert!(
5901                instance
5902                    .call_method1(method, (argument,))
5903                    .unwrap()
5904                    .is_none()
5905            );
5906        });
5907    }
5908}