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 chrono::{DateTime, Utc};
27use indexmap::IndexMap;
28use nautilus_core::{
29    from_pydict,
30    nanos::UnixNanos,
31    python::{IntoPyObjectNautilusExt, to_pyruntime_err, to_pyvalue_err},
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, OrderBookDeltas, 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    prelude::*,
58    types::{PyBytes, PyDict, PyList},
59};
60use ustr::Ustr;
61
62use crate::{
63    actor::{
64        Actor, DataActor, DataActorNative,
65        data_actor::{DataActorConfig, DataActorCore, ImportableActorConfig},
66        registry::{try_get_actor_unchecked, with_actor_registry},
67    },
68    cache::Cache,
69    clock::Clock,
70    component::{Component, with_component_registry},
71    enums::ComponentState,
72    logging::{CMD, RECV},
73    messages::execution::TradingCommand,
74    msgbus::{self, ShareableMessageHandler},
75    python::{
76        cache::PyCache,
77        clock::PyClock,
78        indicators::{registered_python_indicators, wrap_python_indicator},
79        logging::PyLogger,
80    },
81    signal::Signal,
82    timer::{TimeEvent, TimeEventCallback},
83};
84
85#[pyo3::pymethods]
86#[pyo3_stub_gen::derive::gen_stub_pymethods]
87impl DataActorConfig {
88    /// Common configuration for `DataActor` based components.
89    #[new]
90    #[pyo3(signature = (actor_id=None, log_events=true, log_commands=true, **_kwargs))]
91    fn py_new(
92        actor_id: Option<ActorId>,
93        log_events: bool,
94        log_commands: bool,
95        _kwargs: Option<&Bound<'_, PyDict>>,
96    ) -> Self {
97        Self {
98            actor_id,
99            log_events,
100            log_commands,
101        }
102    }
103}
104
105#[pyo3::pymethods]
106#[pyo3_stub_gen::derive::gen_stub_pymethods]
107impl ImportableActorConfig {
108    /// Configuration for creating actors from importable paths.
109    #[new]
110    #[expect(clippy::needless_pass_by_value)]
111    fn py_new(actor_path: String, config_path: String, config: Py<PyDict>) -> PyResult<Self> {
112        let json_config = Python::attach(|py| -> PyResult<HashMap<String, serde_json::Value>> {
113            let kwargs = PyDict::new(py);
114            kwargs.set_item("default", py.eval(pyo3::ffi::c_str!("str"), None, None)?)?;
115            let json_str: String = PyModule::import(py, "json")?
116                .call_method("dumps", (config.bind(py),), Some(&kwargs))?
117                .extract()?;
118
119            let json_value: serde_json::Value =
120                serde_json::from_str(&json_str).map_err(to_pyvalue_err)?;
121
122            if let serde_json::Value::Object(map) = json_value {
123                Ok(map.into_iter().collect())
124            } else {
125                Err(to_pyvalue_err("Config must be a dictionary"))
126            }
127        })?;
128
129        Ok(Self {
130            actor_path,
131            config_path,
132            config: json_config,
133        })
134    }
135
136    #[getter]
137    fn actor_path(&self) -> &String {
138        &self.actor_path
139    }
140
141    #[getter]
142    fn config_path(&self) -> &String {
143        &self.config_path
144    }
145
146    #[getter]
147    fn config(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
148        // Convert HashMap<String, serde_json::Value> back to Python dict
149        let py_dict = PyDict::new(py);
150
151        for (key, value) in &self.config {
152            // Convert serde_json::Value back to Python object via JSON
153            let json_str = serde_json::to_string(value).map_err(to_pyvalue_err)?;
154            let py_value = PyModule::import(py, "json")?.call_method("loads", (json_str,), None)?;
155            py_dict.set_item(key, py_value)?;
156        }
157        Ok(py_dict.unbind())
158    }
159}
160
161/// Inner state of `PyDataActor`, shared between Python wrapper and Rust registries.
162///
163/// This type holds the actual actor state and implements all the actor traits.
164/// It is wrapped in `Rc<UnsafeCell<>>` to allow shared ownership between Python
165/// and the global registries without copying.
166pub struct PyDataActorInner {
167    core: DataActorCore,
168    py_self: Option<Py<PyAny>>,
169    config: Option<Py<PyAny>>,
170    clock: PyClock,
171    logger: PyLogger,
172}
173
174impl Debug for PyDataActorInner {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        f.debug_struct(stringify!(PyDataActorInner))
177            .field("core", &self.core)
178            .field("py_self", &self.py_self.as_ref().map(|_| "<Py<PyAny>>"))
179            .field("config", &self.config.as_ref().map(|_| "<Py<PyAny>>"))
180            .field("clock", &self.clock)
181            .field("logger", &self.logger)
182            .finish()
183    }
184}
185
186impl Deref for PyDataActorInner {
187    type Target = DataActorCore;
188
189    fn deref(&self) -> &Self::Target {
190        &self.core
191    }
192}
193
194impl DerefMut for PyDataActorInner {
195    fn deref_mut(&mut self) -> &mut Self::Target {
196        &mut self.core
197    }
198}
199
200impl DataActorNative for PyDataActorInner {
201    fn core(&self) -> &DataActorCore {
202        &self.core
203    }
204
205    fn core_mut(&mut self) -> &mut DataActorCore {
206        &mut self.core
207    }
208}
209
210#[expect(clippy::needless_pass_by_ref_mut)]
211impl PyDataActorInner {
212    fn execute_exec_algorithm_command(&mut self, command: &TradingCommand) -> anyhow::Result<()> {
213        if self.core.config.log_commands {
214            let id = self.core.actor_id;
215            log::info!("{id} {RECV}{CMD} {command:?}");
216        }
217
218        if self.core.state() != ComponentState::Running {
219            return Ok(());
220        }
221
222        match command {
223            TradingCommand::SubmitOrder(cmd) => {
224                let order = DataActor::cache(self).try_order(&cmd.client_order_id)?;
225                self.dispatch_on_order(order)
226                    .map_err(|e| anyhow::anyhow!("Python on_order failed: {e}"))
227            }
228            TradingCommand::SubmitOrderList(cmd) => {
229                let orders = self.orders_for_list(&cmd.order_list)?;
230                self.dispatch_on_order_list(cmd.order_list.clone(), orders)
231                    .map_err(|e| anyhow::anyhow!("Python on_order_list failed: {e}"))
232            }
233            _ => {
234                log::warn!("Unhandled command type: {command:?}");
235                Ok(())
236            }
237        }
238    }
239
240    fn orders_for_list(&self, order_list: &OrderList) -> anyhow::Result<Vec<OrderAny>> {
241        let cache = DataActor::cache(self);
242        let mut orders = Vec::with_capacity(order_list.client_order_ids.len());
243
244        for client_order_id in &order_list.client_order_ids {
245            orders.push(cache.try_order(client_order_id)?);
246        }
247
248        Ok(orders)
249    }
250
251    fn dispatch_on_order(&mut self, order: OrderAny) -> PyResult<()> {
252        if let Some(ref py_self) = self.py_self {
253            Python::attach(|py| -> PyResult<()> {
254                let py_order = order_any_to_pyobject(py, order)?;
255                py_self.call_method1(py, "on_order", (py_order,))?;
256                Ok(())
257            })?;
258        }
259        Ok(())
260    }
261
262    fn dispatch_on_order_list(
263        &mut self,
264        order_list: OrderList,
265        orders: Vec<OrderAny>,
266    ) -> PyResult<()> {
267        if let Some(ref py_self) = self.py_self {
268            Python::attach(|py| -> PyResult<()> {
269                if py_self.bind(py).hasattr("on_order_list")? {
270                    let py_order_list = order_list.into_py_any_unwrap(py);
271                    let py_orders = orders
272                        .into_iter()
273                        .map(|order| order_any_to_pyobject(py, order))
274                        .collect::<PyResult<Vec<_>>>()?;
275                    let py_orders = PyList::new(py, py_orders)?;
276
277                    py_self.call_method1(py, "on_order_list", (py_order_list, py_orders))?;
278                } else {
279                    for order in orders {
280                        let py_order = order_any_to_pyobject(py, order)?;
281                        py_self.call_method1(py, "on_order", (py_order,))?;
282                    }
283                }
284                Ok(())
285            })?;
286        }
287        Ok(())
288    }
289
290    fn dispatch_on_start(&self) -> PyResult<()> {
291        if let Some(ref py_self) = self.py_self {
292            Python::attach(|py| py_self.call_method0(py, "on_start"))?;
293        }
294        Ok(())
295    }
296
297    fn dispatch_on_stop(&mut self) -> PyResult<()> {
298        if let Some(ref py_self) = self.py_self {
299            Python::attach(|py| py_self.call_method0(py, "on_stop"))?;
300        }
301        Ok(())
302    }
303
304    fn dispatch_on_resume(&mut self) -> PyResult<()> {
305        if let Some(ref py_self) = self.py_self {
306            Python::attach(|py| py_self.call_method0(py, "on_resume"))?;
307        }
308        Ok(())
309    }
310
311    fn dispatch_on_reset(&mut self) -> PyResult<()> {
312        if let Some(ref py_self) = self.py_self {
313            Python::attach(|py| py_self.call_method0(py, "on_reset"))?;
314        }
315        Ok(())
316    }
317
318    fn dispatch_on_dispose(&mut self) -> PyResult<()> {
319        if let Some(ref py_self) = self.py_self {
320            Python::attach(|py| py_self.call_method0(py, "on_dispose"))?;
321        }
322        Ok(())
323    }
324
325    fn dispatch_on_degrade(&mut self) -> PyResult<()> {
326        if let Some(ref py_self) = self.py_self {
327            Python::attach(|py| py_self.call_method0(py, "on_degrade"))?;
328        }
329        Ok(())
330    }
331
332    fn dispatch_on_fault(&mut self) -> PyResult<()> {
333        if let Some(ref py_self) = self.py_self {
334            Python::attach(|py| py_self.call_method0(py, "on_fault"))?;
335        }
336        Ok(())
337    }
338
339    fn dispatch_on_save(&self) -> PyResult<IndexMap<String, Vec<u8>>> {
340        if let Some(ref py_self) = self.py_self {
341            Python::attach(|py| {
342                let py_state = py_self.call_method0(py, "on_save")?;
343                let py_state: &Bound<'_, PyDict> = py_state.cast_bound::<PyDict>(py)?;
344                pydict_to_state(py_state)
345            })
346        } else {
347            Ok(IndexMap::new())
348        }
349    }
350
351    fn dispatch_on_load(&mut self, state: &IndexMap<String, Vec<u8>>) -> PyResult<()> {
352        if let Some(ref py_self) = self.py_self {
353            Python::attach(|py| -> PyResult<()> {
354                let py_state = state_to_pydict(py, state)?;
355                py_self.call_method1(py, "on_load", (py_state,))?;
356                Ok(())
357            })?;
358        }
359        Ok(())
360    }
361
362    fn dispatch_on_time_event(&mut self, event: TimeEvent) -> PyResult<()> {
363        if let Some(ref py_self) = self.py_self {
364            Python::attach(|py| {
365                py_self.call_method1(py, "on_time_event", (event.into_py_any_unwrap(py),))
366            })?;
367        }
368        Ok(())
369    }
370
371    fn dispatch_on_data(&mut self, data: Py<PyAny>) -> PyResult<()> {
372        if let Some(ref py_self) = self.py_self {
373            Python::attach(|py| py_self.call_method1(py, "on_data", (data,)))?;
374        }
375        Ok(())
376    }
377
378    fn dispatch_on_signal(&mut self, signal: &Signal) -> PyResult<()> {
379        if let Some(ref py_self) = self.py_self {
380            Python::attach(|py| {
381                py_self.call_method1(py, "on_signal", (signal.clone().into_py_any_unwrap(py),))
382            })?;
383        }
384        Ok(())
385    }
386
387    fn dispatch_on_instrument(&mut self, instrument: Py<PyAny>) -> PyResult<()> {
388        if let Some(ref py_self) = self.py_self {
389            Python::attach(|py| py_self.call_method1(py, "on_instrument", (instrument,)))?;
390        }
391        Ok(())
392    }
393
394    fn dispatch_on_quote(&mut self, quote: QuoteTick) -> PyResult<()> {
395        if let Some(ref py_self) = self.py_self {
396            Python::attach(|py| {
397                py_self.call_method1(py, "on_quote", (quote.into_py_any_unwrap(py),))
398            })?;
399        }
400        Ok(())
401    }
402
403    fn dispatch_on_trade(&mut self, trade: TradeTick) -> PyResult<()> {
404        if let Some(ref py_self) = self.py_self {
405            Python::attach(|py| {
406                py_self.call_method1(py, "on_trade", (trade.into_py_any_unwrap(py),))
407            })?;
408        }
409        Ok(())
410    }
411
412    fn dispatch_on_bar(&mut self, bar: Bar) -> PyResult<()> {
413        if let Some(ref py_self) = self.py_self {
414            Python::attach(|py| py_self.call_method1(py, "on_bar", (bar.into_py_any_unwrap(py),)))?;
415        }
416        Ok(())
417    }
418
419    fn dispatch_on_book_deltas(&mut self, deltas: OrderBookDeltas) -> PyResult<()> {
420        if let Some(ref py_self) = self.py_self {
421            Python::attach(|py| {
422                py_self.call_method1(py, "on_book_deltas", (deltas.into_py_any_unwrap(py),))
423            })?;
424        }
425        Ok(())
426    }
427
428    fn dispatch_on_book(&mut self, book: &OrderBook) -> PyResult<()> {
429        if let Some(ref py_self) = self.py_self {
430            Python::attach(|py| {
431                py_self.call_method1(py, "on_book", (book.clone().into_py_any_unwrap(py),))
432            })?;
433        }
434        Ok(())
435    }
436
437    fn dispatch_on_mark_price(&mut self, mark_price: MarkPriceUpdate) -> PyResult<()> {
438        if let Some(ref py_self) = self.py_self {
439            Python::attach(|py| {
440                py_self.call_method1(py, "on_mark_price", (mark_price.into_py_any_unwrap(py),))
441            })?;
442        }
443        Ok(())
444    }
445
446    fn dispatch_on_index_price(&mut self, index_price: IndexPriceUpdate) -> PyResult<()> {
447        if let Some(ref py_self) = self.py_self {
448            Python::attach(|py| {
449                py_self.call_method1(py, "on_index_price", (index_price.into_py_any_unwrap(py),))
450            })?;
451        }
452        Ok(())
453    }
454
455    fn dispatch_on_funding_rate(&mut self, funding_rate: FundingRateUpdate) -> PyResult<()> {
456        if let Some(ref py_self) = self.py_self {
457            Python::attach(|py| {
458                py_self.call_method1(
459                    py,
460                    "on_funding_rate",
461                    (funding_rate.into_py_any_unwrap(py),),
462                )
463            })?;
464        }
465        Ok(())
466    }
467
468    fn dispatch_on_instrument_status(&mut self, data: InstrumentStatus) -> PyResult<()> {
469        if let Some(ref py_self) = self.py_self {
470            Python::attach(|py| {
471                py_self.call_method1(py, "on_instrument_status", (data.into_py_any_unwrap(py),))
472            })?;
473        }
474        Ok(())
475    }
476
477    fn dispatch_on_instrument_close(&mut self, update: InstrumentClose) -> PyResult<()> {
478        if let Some(ref py_self) = self.py_self {
479            Python::attach(|py| {
480                py_self.call_method1(py, "on_instrument_close", (update.into_py_any_unwrap(py),))
481            })?;
482        }
483        Ok(())
484    }
485
486    fn dispatch_on_option_greeks(&mut self, greeks: OptionGreeks) -> PyResult<()> {
487        if let Some(ref py_self) = self.py_self {
488            Python::attach(|py| {
489                py_self.call_method1(py, "on_option_greeks", (greeks.into_py_any_unwrap(py),))
490            })?;
491        }
492        Ok(())
493    }
494
495    fn dispatch_on_option_chain(&mut self, slice: OptionChainSlice) -> PyResult<()> {
496        if let Some(ref py_self) = self.py_self {
497            Python::attach(|py| {
498                py_self.call_method1(py, "on_option_chain", (slice.into_py_any_unwrap(py),))
499            })?;
500        }
501        Ok(())
502    }
503
504    fn dispatch_on_historical_data(&mut self, data: Py<PyAny>) -> PyResult<()> {
505        if let Some(ref py_self) = self.py_self {
506            Python::attach(|py| py_self.call_method1(py, "on_historical_data", (data,)))?;
507        }
508        Ok(())
509    }
510
511    fn dispatch_on_historical_quotes(&mut self, quotes: Vec<QuoteTick>) -> PyResult<()> {
512        if let Some(ref py_self) = self.py_self {
513            Python::attach(|py| {
514                let py_quotes: Vec<_> = quotes
515                    .into_iter()
516                    .map(|q| q.into_py_any_unwrap(py))
517                    .collect();
518                py_self.call_method1(py, "on_historical_quotes", (py_quotes,))
519            })?;
520        }
521        Ok(())
522    }
523
524    fn dispatch_on_historical_trades(&mut self, trades: Vec<TradeTick>) -> PyResult<()> {
525        if let Some(ref py_self) = self.py_self {
526            Python::attach(|py| {
527                let py_trades: Vec<_> = trades
528                    .into_iter()
529                    .map(|t| t.into_py_any_unwrap(py))
530                    .collect();
531                py_self.call_method1(py, "on_historical_trades", (py_trades,))
532            })?;
533        }
534        Ok(())
535    }
536
537    fn dispatch_on_historical_funding_rates(
538        &mut self,
539        funding_rates: Vec<FundingRateUpdate>,
540    ) -> PyResult<()> {
541        if let Some(ref py_self) = self.py_self {
542            Python::attach(|py| {
543                let py_rates: Vec<_> = funding_rates
544                    .into_iter()
545                    .map(|r| r.into_py_any_unwrap(py))
546                    .collect();
547                py_self.call_method1(py, "on_historical_funding_rates", (py_rates,))
548            })?;
549        }
550        Ok(())
551    }
552
553    fn dispatch_on_historical_bars(&mut self, bars: Vec<Bar>) -> PyResult<()> {
554        if let Some(ref py_self) = self.py_self {
555            Python::attach(|py| {
556                let py_bars: Vec<_> = bars.into_iter().map(|b| b.into_py_any_unwrap(py)).collect();
557                py_self.call_method1(py, "on_historical_bars", (py_bars,))
558            })?;
559        }
560        Ok(())
561    }
562
563    fn dispatch_on_historical_mark_prices(
564        &mut self,
565        mark_prices: Vec<MarkPriceUpdate>,
566    ) -> PyResult<()> {
567        if let Some(ref py_self) = self.py_self {
568            Python::attach(|py| {
569                let py_prices: Vec<_> = mark_prices
570                    .into_iter()
571                    .map(|p| p.into_py_any_unwrap(py))
572                    .collect();
573                py_self.call_method1(py, "on_historical_mark_prices", (py_prices,))
574            })?;
575        }
576        Ok(())
577    }
578
579    fn dispatch_on_historical_index_prices(
580        &mut self,
581        index_prices: Vec<IndexPriceUpdate>,
582    ) -> PyResult<()> {
583        if let Some(ref py_self) = self.py_self {
584            Python::attach(|py| {
585                let py_prices: Vec<_> = index_prices
586                    .into_iter()
587                    .map(|p| p.into_py_any_unwrap(py))
588                    .collect();
589                py_self.call_method1(py, "on_historical_index_prices", (py_prices,))
590            })?;
591        }
592        Ok(())
593    }
594
595    #[cfg(feature = "defi")]
596    fn dispatch_on_block(&mut self, block: Block) -> PyResult<()> {
597        if let Some(ref py_self) = self.py_self {
598            Python::attach(|py| {
599                py_self.call_method1(py, "on_block", (block.into_py_any_unwrap(py),))
600            })?;
601        }
602        Ok(())
603    }
604
605    #[cfg(feature = "defi")]
606    fn dispatch_on_pool(&mut self, pool: Pool) -> PyResult<()> {
607        if let Some(ref py_self) = self.py_self {
608            Python::attach(|py| {
609                py_self.call_method1(py, "on_pool", (pool.into_py_any_unwrap(py),))
610            })?;
611        }
612        Ok(())
613    }
614
615    #[cfg(feature = "defi")]
616    fn dispatch_on_pool_swap(&mut self, swap: PoolSwap) -> PyResult<()> {
617        if let Some(ref py_self) = self.py_self {
618            Python::attach(|py| {
619                py_self.call_method1(py, "on_pool_swap", (swap.into_py_any_unwrap(py),))
620            })?;
621        }
622        Ok(())
623    }
624
625    #[cfg(feature = "defi")]
626    fn dispatch_on_pool_liquidity_update(&mut self, update: PoolLiquidityUpdate) -> PyResult<()> {
627        if let Some(ref py_self) = self.py_self {
628            Python::attach(|py| {
629                py_self.call_method1(
630                    py,
631                    "on_pool_liquidity_update",
632                    (update.into_py_any_unwrap(py),),
633                )
634            })?;
635        }
636        Ok(())
637    }
638
639    #[cfg(feature = "defi")]
640    fn dispatch_on_pool_fee_collect(&mut self, collect: PoolFeeCollect) -> PyResult<()> {
641        if let Some(ref py_self) = self.py_self {
642            Python::attach(|py| {
643                py_self.call_method1(py, "on_pool_fee_collect", (collect.into_py_any_unwrap(py),))
644            })?;
645        }
646        Ok(())
647    }
648
649    #[cfg(feature = "defi")]
650    fn dispatch_on_pool_flash(&mut self, flash: PoolFlash) -> PyResult<()> {
651        if let Some(ref py_self) = self.py_self {
652            Python::attach(|py| {
653                py_self.call_method1(py, "on_pool_flash", (flash.into_py_any_unwrap(py),))
654            })?;
655        }
656        Ok(())
657    }
658}
659
660fn dict_to_params(
661    py: Python<'_>,
662    params: Option<Py<PyDict>>,
663) -> PyResult<Option<nautilus_core::Params>> {
664    match params {
665        Some(dict) => from_pydict(py, &dict),
666        None => Ok(None),
667    }
668}
669
670fn state_to_pydict(py: Python<'_>, state: &IndexMap<String, Vec<u8>>) -> PyResult<Py<PyDict>> {
671    let py_state = PyDict::new(py);
672    for (key, value) in state {
673        py_state.set_item(key, PyBytes::new(py, value))?;
674    }
675    Ok(py_state.unbind())
676}
677
678fn pydict_to_state(state: &Bound<'_, PyDict>) -> PyResult<IndexMap<String, Vec<u8>>> {
679    let mut rust_state = IndexMap::with_capacity(state.len());
680    for (key, value) in state.iter() {
681        rust_state.insert(key.extract()?, value.extract()?);
682    }
683    Ok(rust_state)
684}
685
686/// Python-facing wrapper for `DataActor`.
687///
688/// This wrapper holds shared ownership of `PyDataActorInner` via `Rc<UnsafeCell<>>`.
689/// Both Python (through this wrapper) and the global registries share the same
690/// underlying actor instance, ensuring mutations are visible from both sides.
691#[allow(non_camel_case_types)]
692#[pyo3::pyclass(
693    module = "nautilus_trader.common",
694    name = "DataActor",
695    unsendable,
696    subclass
697)]
698#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")]
699pub struct PyDataActor {
700    inner: Rc<UnsafeCell<PyDataActorInner>>,
701}
702
703impl Debug for PyDataActor {
704    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
705        f.debug_struct(stringify!(PyDataActor))
706            .field("inner", &self.inner())
707            .finish()
708    }
709}
710
711impl PyDataActor {
712    /// Returns a reference to the inner actor state.
713    ///
714    /// # Safety
715    ///
716    /// This is safe for single-threaded use. The `UnsafeCell` allows interior
717    /// mutability which is required for the registries to mutate the actor.
718    #[inline]
719    #[allow(unsafe_code)]
720    pub(crate) fn inner(&self) -> &PyDataActorInner {
721        unsafe { &*self.inner.get() }
722    }
723
724    /// Returns a mutable reference to the inner actor state.
725    ///
726    /// # Safety
727    ///
728    /// This is safe for single-threaded use. Callers must ensure no aliasing
729    /// mutable references exist.
730    #[inline]
731    #[allow(unsafe_code, clippy::mut_from_ref)]
732    pub(crate) fn inner_mut(&self) -> &mut PyDataActorInner {
733        unsafe { &mut *self.inner.get() }
734    }
735}
736
737impl Deref for PyDataActor {
738    type Target = DataActorCore;
739
740    fn deref(&self) -> &Self::Target {
741        &self.inner().core
742    }
743}
744
745impl DerefMut for PyDataActor {
746    fn deref_mut(&mut self) -> &mut Self::Target {
747        &mut self.inner_mut().core
748    }
749}
750
751impl PyDataActor {
752    // Rust constructor for tests and direct Rust usage
753    pub fn new(config: Option<DataActorConfig>) -> Self {
754        let config = config.unwrap_or_default();
755        let core = DataActorCore::new(config);
756        let clock = PyClock::new_test(); // Temporary clock, will be updated on registration
757        let logger = PyLogger::new(core.actor_id().as_str());
758
759        let inner = PyDataActorInner {
760            core,
761            py_self: None,
762            config: None,
763            clock,
764            logger,
765        };
766
767        Self {
768            inner: Rc::new(UnsafeCell::new(inner)),
769        }
770    }
771
772    /// Sets the Python instance reference for method dispatch.
773    ///
774    /// This enables the `PyDataActor` to forward method calls (like `on_start`, `on_stop`)
775    /// to the original Python instance that contains this `PyDataActor`. This is essential
776    /// for Python inheritance to work correctly, allowing Python subclasses to override
777    /// `DataActor` methods and have them called by the Rust system.
778    pub fn set_python_instance(&mut self, py_obj: Py<PyAny>) {
779        self.inner_mut().py_self = Some(py_obj);
780    }
781
782    /// Stores the original Python config object passed at construction.
783    ///
784    /// Retained so the constructed instance exposes `.config` (matching v1) and so
785    /// instance-based registration can source the actor ID and logging flags from the
786    /// same single config object.
787    pub fn set_config(&mut self, config: Option<Py<PyAny>>) {
788        self.inner_mut().config = config;
789    }
790
791    /// Updates the `actor_id` in both the core config and the `actor_id` field.
792    ///
793    /// This method is only exposed for the Python actor to assist with configuration and should
794    /// **never** be called post registration. Calling this after registration will cause
795    /// inconsistent state where the actor is registered under one ID but its internal `actor_id`
796    /// field contains another, breaking message routing and lifecycle management.
797    pub fn set_actor_id(&mut self, actor_id: ActorId) {
798        let inner = self.inner_mut();
799        inner.core.config.actor_id = Some(actor_id);
800        inner.core.actor_id = actor_id;
801    }
802
803    /// Updates the `log_events` setting in the core config.
804    pub fn set_log_events(&mut self, log_events: bool) {
805        self.inner_mut().core.config.log_events = log_events;
806    }
807
808    /// Updates the `log_commands` setting in the core config.
809    pub fn set_log_commands(&mut self, log_commands: bool) {
810        self.inner_mut().core.config.log_commands = log_commands;
811    }
812
813    /// Returns the memory address of this instance as a hexadecimal string.
814    pub fn mem_address(&self) -> String {
815        self.inner().core.mem_address()
816    }
817
818    /// Returns a value indicating whether the actor has been registered with a trader.
819    pub fn is_registered(&self) -> bool {
820        self.inner().core.is_registered()
821    }
822
823    /// Register the actor with a trader.
824    ///
825    /// # Errors
826    ///
827    /// Returns an error if the actor is already registered or if the registration process fails.
828    pub fn register(
829        &mut self,
830        trader_id: TraderId,
831        clock: Rc<RefCell<dyn Clock>>,
832        cache: Rc<RefCell<Cache>>,
833    ) -> anyhow::Result<()> {
834        let inner = self.inner_mut();
835        inner.core.register(trader_id, clock, cache)?;
836
837        inner.clock = PyClock::from_rc(inner.core.clock_rc());
838
839        // Register default time event handler for this actor
840        let actor_id = inner.actor_id().inner();
841        let callback = TimeEventCallback::from(move |event: TimeEvent| {
842            if let Some(mut actor) = try_get_actor_unchecked::<PyDataActorInner>(&actor_id) {
843                if let Err(e) = actor.on_time_event(&event) {
844                    log::error!("Python time event handler failed for actor {actor_id}: {e}");
845                }
846            } else {
847                log::error!("Actor {actor_id} not found for time event handling");
848            }
849        });
850
851        inner.clock.inner_mut().register_default_handler(callback);
852
853        inner.initialize()
854    }
855
856    /// Registers this actor in the global component and actor registries.
857    ///
858    /// Clones the internal `Rc` and inserts into both registries. This ensures
859    /// Python and the registries share the exact same actor instance.
860    pub fn register_in_global_registries(&self) {
861        let inner = self.inner();
862        let component_id = inner.component_id().inner();
863        let actor_id = Actor::id(inner);
864
865        let inner_ref: Rc<UnsafeCell<PyDataActorInner>> = self.inner.clone();
866
867        let component_trait_ref: Rc<UnsafeCell<dyn Component>> = inner_ref.clone();
868        with_component_registry(|registry| registry.insert(component_id, component_trait_ref));
869
870        let actor_trait_ref: Rc<UnsafeCell<dyn Actor>> = inner_ref;
871        with_actor_registry(|registry| registry.insert(actor_id, actor_trait_ref));
872    }
873}
874
875pub fn register_python_exec_algorithm_endpoint(exec_algorithm_id: ExecAlgorithmId) {
876    let actor_id = Ustr::from(exec_algorithm_id.inner().as_str());
877    let endpoint: Ustr = format!("{exec_algorithm_id}.execute").into();
878    let handler = ShareableMessageHandler::from_typed(move |command: &TradingCommand| {
879        if let Some(mut algo) = try_get_actor_unchecked::<PyDataActorInner>(&actor_id) {
880            if let Err(e) = algo.execute_exec_algorithm_command(command) {
881                log::error!("Error executing command on Python algorithm {actor_id}: {e}");
882            }
883        } else {
884            log::error!("Python execution algorithm {actor_id} not found in registry");
885        }
886    });
887    msgbus::register_any(endpoint.into(), handler);
888}
889
890impl DataActor for PyDataActorInner {
891    fn on_start(&mut self) -> anyhow::Result<()> {
892        self.dispatch_on_start()
893            .map_err(|e| anyhow::anyhow!("Python on_start failed: {e}"))
894    }
895
896    fn on_stop(&mut self) -> anyhow::Result<()> {
897        self.dispatch_on_stop()
898            .map_err(|e| anyhow::anyhow!("Python on_stop failed: {e}"))
899    }
900
901    fn on_resume(&mut self) -> anyhow::Result<()> {
902        self.dispatch_on_resume()
903            .map_err(|e| anyhow::anyhow!("Python on_resume failed: {e}"))
904    }
905
906    fn on_reset(&mut self) -> anyhow::Result<()> {
907        self.dispatch_on_reset()
908            .map_err(|e| anyhow::anyhow!("Python on_reset failed: {e}"))
909    }
910
911    fn on_dispose(&mut self) -> anyhow::Result<()> {
912        self.dispatch_on_dispose()
913            .map_err(|e| anyhow::anyhow!("Python on_dispose failed: {e}"))
914    }
915
916    fn on_degrade(&mut self) -> anyhow::Result<()> {
917        self.dispatch_on_degrade()
918            .map_err(|e| anyhow::anyhow!("Python on_degrade failed: {e}"))
919    }
920
921    fn on_fault(&mut self) -> anyhow::Result<()> {
922        self.dispatch_on_fault()
923            .map_err(|e| anyhow::anyhow!("Python on_fault failed: {e}"))
924    }
925
926    fn on_save(&self) -> anyhow::Result<IndexMap<String, Vec<u8>>> {
927        self.dispatch_on_save()
928            .map_err(|e| anyhow::anyhow!("Python on_save failed: {e}"))
929    }
930
931    fn on_load(&mut self, state: IndexMap<String, Vec<u8>>) -> anyhow::Result<()> {
932        self.dispatch_on_load(&state)
933            .map_err(|e| anyhow::anyhow!("Python on_load failed: {e}"))
934    }
935
936    fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()> {
937        self.dispatch_on_time_event(event.clone())
938            .map_err(|e| anyhow::anyhow!("Python on_time_event failed: {e}"))
939    }
940
941    #[allow(unused_variables)]
942    fn on_data(&mut self, data: &CustomData) -> anyhow::Result<()> {
943        Python::attach(|py| {
944            let py_data: Py<PyAny> = Py::new(py, data.clone())?.into_any();
945            self.dispatch_on_data(py_data)
946                .map_err(|e| anyhow::anyhow!("Python on_data failed: {e}"))
947        })
948    }
949
950    fn on_signal(&mut self, signal: &Signal) -> anyhow::Result<()> {
951        self.dispatch_on_signal(signal)
952            .map_err(|e| anyhow::anyhow!("Python on_signal failed: {e}"))
953    }
954
955    fn on_instrument(&mut self, instrument: &InstrumentAny) -> anyhow::Result<()> {
956        Python::attach(|py| {
957            let py_instrument = instrument_any_to_pyobject(py, instrument.clone())
958                .map_err(|e| anyhow::anyhow!("Failed to convert InstrumentAny to Python: {e}"))?;
959            self.dispatch_on_instrument(py_instrument)
960                .map_err(|e| anyhow::anyhow!("Python on_instrument failed: {e}"))
961        })
962    }
963
964    fn on_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
965        self.dispatch_on_quote(*quote)
966            .map_err(|e| anyhow::anyhow!("Python on_quote failed: {e}"))
967    }
968
969    fn on_trade(&mut self, tick: &TradeTick) -> anyhow::Result<()> {
970        self.dispatch_on_trade(*tick)
971            .map_err(|e| anyhow::anyhow!("Python on_trade failed: {e}"))
972    }
973
974    fn on_bar(&mut self, bar: &Bar) -> anyhow::Result<()> {
975        self.dispatch_on_bar(*bar)
976            .map_err(|e| anyhow::anyhow!("Python on_bar failed: {e}"))
977    }
978
979    fn on_book_deltas(&mut self, deltas: &OrderBookDeltas) -> anyhow::Result<()> {
980        self.dispatch_on_book_deltas(deltas.clone())
981            .map_err(|e| anyhow::anyhow!("Python on_book_deltas failed: {e}"))
982    }
983
984    fn on_book(&mut self, order_book: &OrderBook) -> anyhow::Result<()> {
985        self.dispatch_on_book(order_book)
986            .map_err(|e| anyhow::anyhow!("Python on_book failed: {e}"))
987    }
988
989    fn on_mark_price(&mut self, mark_price: &MarkPriceUpdate) -> anyhow::Result<()> {
990        self.dispatch_on_mark_price(*mark_price)
991            .map_err(|e| anyhow::anyhow!("Python on_mark_price failed: {e}"))
992    }
993
994    fn on_index_price(&mut self, index_price: &IndexPriceUpdate) -> anyhow::Result<()> {
995        self.dispatch_on_index_price(*index_price)
996            .map_err(|e| anyhow::anyhow!("Python on_index_price failed: {e}"))
997    }
998
999    fn on_funding_rate(&mut self, funding_rate: &FundingRateUpdate) -> anyhow::Result<()> {
1000        self.dispatch_on_funding_rate(*funding_rate)
1001            .map_err(|e| anyhow::anyhow!("Python on_funding_rate failed: {e}"))
1002    }
1003
1004    fn on_instrument_status(&mut self, data: &InstrumentStatus) -> anyhow::Result<()> {
1005        self.dispatch_on_instrument_status(*data)
1006            .map_err(|e| anyhow::anyhow!("Python on_instrument_status failed: {e}"))
1007    }
1008
1009    fn on_instrument_close(&mut self, update: &InstrumentClose) -> anyhow::Result<()> {
1010        self.dispatch_on_instrument_close(*update)
1011            .map_err(|e| anyhow::anyhow!("Python on_instrument_close failed: {e}"))
1012    }
1013
1014    fn on_option_greeks(&mut self, greeks: &OptionGreeks) -> anyhow::Result<()> {
1015        self.dispatch_on_option_greeks(*greeks)
1016            .map_err(|e| anyhow::anyhow!("Python on_option_greeks failed: {e}"))
1017    }
1018
1019    fn on_option_chain(&mut self, slice: &OptionChainSlice) -> anyhow::Result<()> {
1020        self.dispatch_on_option_chain(slice.clone())
1021            .map_err(|e| anyhow::anyhow!("Python on_option_chain failed: {e}"))
1022    }
1023
1024    #[cfg(feature = "defi")]
1025    fn on_block(&mut self, block: &Block) -> anyhow::Result<()> {
1026        self.dispatch_on_block(block.clone())
1027            .map_err(|e| anyhow::anyhow!("Python on_block failed: {e}"))
1028    }
1029
1030    #[cfg(feature = "defi")]
1031    fn on_pool(&mut self, pool: &Pool) -> anyhow::Result<()> {
1032        self.dispatch_on_pool(pool.clone())
1033            .map_err(|e| anyhow::anyhow!("Python on_pool failed: {e}"))
1034    }
1035
1036    #[cfg(feature = "defi")]
1037    fn on_pool_swap(&mut self, swap: &PoolSwap) -> anyhow::Result<()> {
1038        self.dispatch_on_pool_swap(swap.clone())
1039            .map_err(|e| anyhow::anyhow!("Python on_pool_swap failed: {e}"))
1040    }
1041
1042    #[cfg(feature = "defi")]
1043    fn on_pool_liquidity_update(&mut self, update: &PoolLiquidityUpdate) -> anyhow::Result<()> {
1044        self.dispatch_on_pool_liquidity_update(update.clone())
1045            .map_err(|e| anyhow::anyhow!("Python on_pool_liquidity_update failed: {e}"))
1046    }
1047
1048    #[cfg(feature = "defi")]
1049    fn on_pool_fee_collect(&mut self, collect: &PoolFeeCollect) -> anyhow::Result<()> {
1050        self.dispatch_on_pool_fee_collect(collect.clone())
1051            .map_err(|e| anyhow::anyhow!("Python on_pool_fee_collect failed: {e}"))
1052    }
1053
1054    #[cfg(feature = "defi")]
1055    fn on_pool_flash(&mut self, flash: &PoolFlash) -> anyhow::Result<()> {
1056        self.dispatch_on_pool_flash(flash.clone())
1057            .map_err(|e| anyhow::anyhow!("Python on_pool_flash failed: {e}"))
1058    }
1059
1060    fn on_historical_data(&mut self, data: &dyn Any) -> anyhow::Result<()> {
1061        Python::attach(|py| {
1062            let py_data: Py<PyAny> = if let Some(custom_data) = data.downcast_ref::<CustomData>() {
1063                Py::new(py, custom_data.clone())?.into_any()
1064            } else {
1065                anyhow::bail!("Failed to convert historical data to Python: unsupported type");
1066            };
1067            self.dispatch_on_historical_data(py_data)
1068                .map_err(|e| anyhow::anyhow!("Python on_historical_data failed: {e}"))
1069        })
1070    }
1071
1072    fn on_historical_quotes(&mut self, quotes: &[QuoteTick]) -> anyhow::Result<()> {
1073        self.dispatch_on_historical_quotes(quotes.to_vec())
1074            .map_err(|e| anyhow::anyhow!("Python on_historical_quotes failed: {e}"))
1075    }
1076
1077    fn on_historical_trades(&mut self, trades: &[TradeTick]) -> anyhow::Result<()> {
1078        self.dispatch_on_historical_trades(trades.to_vec())
1079            .map_err(|e| anyhow::anyhow!("Python on_historical_trades failed: {e}"))
1080    }
1081
1082    fn on_historical_funding_rates(
1083        &mut self,
1084        funding_rates: &[FundingRateUpdate],
1085    ) -> anyhow::Result<()> {
1086        self.dispatch_on_historical_funding_rates(funding_rates.to_vec())
1087            .map_err(|e| anyhow::anyhow!("Python on_historical_funding_rates failed: {e}"))
1088    }
1089
1090    fn on_historical_bars(&mut self, bars: &[Bar]) -> anyhow::Result<()> {
1091        self.dispatch_on_historical_bars(bars.to_vec())
1092            .map_err(|e| anyhow::anyhow!("Python on_historical_bars failed: {e}"))
1093    }
1094
1095    fn on_historical_mark_prices(&mut self, mark_prices: &[MarkPriceUpdate]) -> anyhow::Result<()> {
1096        self.dispatch_on_historical_mark_prices(mark_prices.to_vec())
1097            .map_err(|e| anyhow::anyhow!("Python on_historical_mark_prices failed: {e}"))
1098    }
1099
1100    fn on_historical_index_prices(
1101        &mut self,
1102        index_prices: &[IndexPriceUpdate],
1103    ) -> anyhow::Result<()> {
1104        self.dispatch_on_historical_index_prices(index_prices.to_vec())
1105            .map_err(|e| anyhow::anyhow!("Python on_historical_index_prices failed: {e}"))
1106    }
1107}
1108
1109#[pymethods]
1110#[pyo3_stub_gen::derive::gen_stub_pymethods]
1111impl PyDataActor {
1112    /// Creates a new [`PyDataActor`] instance.
1113    ///
1114    /// Accepts `None` or any Python object. If the object is a [`DataActorConfig`]
1115    /// (or can be extracted as one via `from_py_object`), its values are used;
1116    /// otherwise the actor falls back to [`DataActorConfig::default()`].
1117    ///
1118    /// This permissive signature is required so that Python subclasses can pass a
1119    /// **custom** config dataclass to their `__init__`. The original object is retained
1120    /// here in `__new__`, which always receives the constructor arguments, so `.config`
1121    /// and registration see the config even when a subclass omits forwarding it to
1122    /// `super().__init__()`.
1123    #[new]
1124    #[pyo3(signature = (config=None))]
1125    fn py_new(config: Option<Py<PyAny>>) -> Self {
1126        let actor_config = config
1127            .as_ref()
1128            .and_then(|obj| Python::attach(|py| obj.extract::<DataActorConfig>(py).ok()));
1129        let mut actor = Self::new(actor_config);
1130        actor.set_config(config);
1131        actor
1132    }
1133
1134    #[pyo3(signature = (config=None))]
1135    fn __init__(slf: &Bound<'_, Self>, config: Option<Py<PyAny>>) {
1136        let py_self: Py<PyAny> = slf.clone().unbind().into_any();
1137        let mut borrowed = slf.borrow_mut();
1138        borrowed.set_python_instance(py_self);
1139        // `__new__` retained the config; only a forwarded config overrides it
1140        if config.is_some() {
1141            borrowed.set_config(config);
1142        }
1143    }
1144
1145    #[getter]
1146    #[pyo3(name = "clock")]
1147    fn py_clock(&self) -> PyResult<PyClock> {
1148        let inner = self.inner();
1149        if inner.core.is_registered() {
1150            Ok(inner.clock.clone())
1151        } else {
1152            Err(to_pyruntime_err(
1153                "Actor must be registered with a trader before accessing clock",
1154            ))
1155        }
1156    }
1157
1158    #[getter]
1159    #[pyo3(name = "cache")]
1160    fn py_cache(&self) -> PyResult<PyCache> {
1161        let inner = self.inner();
1162        if inner.core.is_registered() {
1163            Ok(PyCache::from_rc(inner.core.cache_rc()))
1164        } else {
1165            Err(to_pyruntime_err(
1166                "Actor must be registered with a trader before accessing cache",
1167            ))
1168        }
1169    }
1170
1171    #[getter]
1172    #[pyo3(name = "log")]
1173    fn py_log(&self) -> PyLogger {
1174        self.inner().logger.clone()
1175    }
1176
1177    #[getter]
1178    #[pyo3(name = "actor_id")]
1179    fn py_actor_id(&self) -> ActorId {
1180        self.inner().core.actor_id
1181    }
1182
1183    #[getter]
1184    #[pyo3(name = "config")]
1185    fn py_config(&self, py: Python<'_>) -> Option<Py<PyAny>> {
1186        self.inner()
1187            .config
1188            .as_ref()
1189            .map(|config| config.clone_ref(py))
1190    }
1191
1192    #[getter]
1193    #[pyo3(name = "trader_id")]
1194    fn py_trader_id(&self) -> Option<TraderId> {
1195        self.inner().core.trader_id()
1196    }
1197
1198    #[pyo3(name = "state")]
1199    fn py_state(&self) -> ComponentState {
1200        Component::state(self.inner())
1201    }
1202
1203    #[pyo3(name = "is_ready")]
1204    fn py_is_ready(&self) -> bool {
1205        Component::is_ready(self.inner())
1206    }
1207
1208    #[pyo3(name = "is_running")]
1209    fn py_is_running(&self) -> bool {
1210        Component::is_running(self.inner())
1211    }
1212
1213    #[pyo3(name = "is_stopped")]
1214    fn py_is_stopped(&self) -> bool {
1215        Component::is_stopped(self.inner())
1216    }
1217
1218    #[pyo3(name = "is_degraded")]
1219    fn py_is_degraded(&self) -> bool {
1220        Component::is_degraded(self.inner())
1221    }
1222
1223    #[pyo3(name = "is_faulted")]
1224    fn py_is_faulted(&self) -> bool {
1225        Component::is_faulted(self.inner())
1226    }
1227
1228    #[pyo3(name = "is_disposed")]
1229    fn py_is_disposed(&self) -> bool {
1230        Component::is_disposed(self.inner())
1231    }
1232
1233    #[pyo3(name = "start")]
1234    fn py_start(&mut self) -> PyResult<()> {
1235        Component::start(self.inner_mut()).map_err(to_pyruntime_err)
1236    }
1237
1238    #[pyo3(name = "stop")]
1239    fn py_stop(&mut self) -> PyResult<()> {
1240        Component::stop(self.inner_mut()).map_err(to_pyruntime_err)
1241    }
1242
1243    #[pyo3(name = "save")]
1244    fn py_save(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
1245        let state = DataActor::on_save(self.inner()).map_err(to_pyruntime_err)?;
1246        state_to_pydict(py, &state)
1247    }
1248
1249    #[pyo3(name = "load")]
1250    fn py_load(&mut self, state: &Bound<'_, PyDict>) -> PyResult<()> {
1251        let state = pydict_to_state(state)?;
1252        DataActor::on_load(self.inner_mut(), state).map_err(to_pyruntime_err)
1253    }
1254
1255    #[pyo3(name = "resume")]
1256    fn py_resume(&mut self) -> PyResult<()> {
1257        Component::resume(self.inner_mut()).map_err(to_pyruntime_err)
1258    }
1259
1260    #[pyo3(name = "reset")]
1261    fn py_reset(&mut self) -> PyResult<()> {
1262        Component::reset(self.inner_mut()).map_err(to_pyruntime_err)
1263    }
1264
1265    #[pyo3(name = "dispose")]
1266    fn py_dispose(&mut self) -> PyResult<()> {
1267        Component::dispose(self.inner_mut()).map_err(to_pyruntime_err)
1268    }
1269
1270    #[pyo3(name = "degrade")]
1271    fn py_degrade(&mut self) -> PyResult<()> {
1272        Component::degrade(self.inner_mut()).map_err(to_pyruntime_err)
1273    }
1274
1275    #[pyo3(name = "fault")]
1276    fn py_fault(&mut self) -> PyResult<()> {
1277        Component::fault(self.inner_mut()).map_err(to_pyruntime_err)
1278    }
1279
1280    #[pyo3(name = "shutdown_system")]
1281    #[pyo3(signature = (reason=None))]
1282    fn py_shutdown_system(&self, reason: Option<String>) {
1283        self.inner().shutdown_system(reason);
1284    }
1285
1286    #[pyo3(name = "publish_data")]
1287    fn py_publish_data(&self, data_type: &DataType, data: &CustomData) {
1288        self.inner().publish_data(data_type, data);
1289    }
1290
1291    #[pyo3(name = "publish_signal")]
1292    #[pyo3(signature = (name, value, ts_event=0))]
1293    #[expect(
1294        clippy::needless_pass_by_value,
1295        reason = "PyO3 accepts an owned PyAny handle for Python signal values"
1296    )]
1297    fn py_publish_signal(
1298        &self,
1299        py: Python<'_>,
1300        name: &str,
1301        value: Py<PyAny>,
1302        ts_event: u64,
1303    ) -> PyResult<()> {
1304        // Accept any int / float / str / bool — match v1 behaviour by coercing with `str(value)`.
1305        let value_str: String = value.bind(py).str()?.extract()?;
1306        self.inner()
1307            .publish_signal(name, value_str, UnixNanos::from(ts_event));
1308        Ok(())
1309    }
1310
1311    #[pyo3(name = "add_synthetic")]
1312    fn py_add_synthetic(&self, synthetic: SyntheticInstrument) -> PyResult<()> {
1313        self.inner()
1314            .add_synthetic(synthetic)
1315            .map_err(to_pyvalue_err)
1316    }
1317
1318    #[pyo3(name = "update_synthetic")]
1319    fn py_update_synthetic(&self, synthetic: SyntheticInstrument) -> PyResult<()> {
1320        self.inner()
1321            .update_synthetic(synthetic)
1322            .map_err(to_pyvalue_err)
1323    }
1324
1325    #[getter]
1326    #[pyo3(name = "registered_indicators")]
1327    fn py_registered_indicators(&self, py: Python<'_>) -> PyResult<Py<PyList>> {
1328        registered_python_indicators(py, self.inner().core.registered_indicators())
1329    }
1330
1331    #[pyo3(name = "indicators_initialized")]
1332    fn py_indicators_initialized(&self, _py: Python<'_>) -> PyResult<bool> {
1333        self.inner()
1334            .core
1335            .indicators_initialized()
1336            .map_err(to_pyruntime_err)
1337    }
1338
1339    #[pyo3(name = "register_indicator_for_quote_ticks")]
1340    fn py_register_indicator_for_quote_ticks(
1341        &mut self,
1342        py: Python<'_>,
1343        instrument_id: InstrumentId,
1344        indicator: Py<PyAny>,
1345    ) {
1346        let indicator = wrap_python_indicator(py, indicator);
1347        self.inner_mut()
1348            .core
1349            .register_indicator_for_quote_ticks(instrument_id, indicator);
1350    }
1351
1352    #[pyo3(name = "register_indicator_for_trade_ticks")]
1353    fn py_register_indicator_for_trade_ticks(
1354        &mut self,
1355        py: Python<'_>,
1356        instrument_id: InstrumentId,
1357        indicator: Py<PyAny>,
1358    ) {
1359        let indicator = wrap_python_indicator(py, indicator);
1360        self.inner_mut()
1361            .core
1362            .register_indicator_for_trade_ticks(instrument_id, indicator);
1363    }
1364
1365    #[pyo3(name = "register_indicator_for_bars")]
1366    fn py_register_indicator_for_bars(
1367        &mut self,
1368        py: Python<'_>,
1369        bar_type: BarType,
1370        indicator: Py<PyAny>,
1371    ) {
1372        let indicator = wrap_python_indicator(py, indicator);
1373        self.inner_mut()
1374            .core
1375            .register_indicator_for_bars(bar_type, indicator);
1376    }
1377
1378    #[pyo3(name = "on_start")]
1379    fn py_on_start(&self) {}
1380
1381    #[pyo3(name = "on_stop")]
1382    fn py_on_stop(&mut self) {}
1383
1384    #[pyo3(name = "on_resume")]
1385    fn py_on_resume(&mut self) {}
1386
1387    #[pyo3(name = "on_reset")]
1388    fn py_on_reset(&mut self) {}
1389
1390    #[pyo3(name = "on_dispose")]
1391    fn py_on_dispose(&mut self) {}
1392
1393    #[pyo3(name = "on_degrade")]
1394    fn py_on_degrade(&mut self) {}
1395
1396    #[pyo3(name = "on_fault")]
1397    fn py_on_fault(&mut self) {}
1398
1399    #[pyo3(name = "on_save")]
1400    fn py_on_save(&self, py: Python<'_>) -> Py<PyDict> {
1401        PyDict::new(py).unbind()
1402    }
1403
1404    #[allow(unused_variables)]
1405    #[pyo3(name = "on_load")]
1406    fn py_on_load(&mut self, state: &Bound<'_, PyDict>) {}
1407
1408    #[allow(unused_variables, clippy::needless_pass_by_value)]
1409    #[pyo3(name = "on_time_event")]
1410    fn py_on_time_event(&mut self, event: TimeEvent) {}
1411
1412    #[allow(unused_variables, clippy::needless_pass_by_value)]
1413    #[pyo3(name = "on_data")]
1414    fn py_on_data(&mut self, data: Py<PyAny>) {}
1415
1416    #[allow(unused_variables)]
1417    #[pyo3(name = "on_signal")]
1418    fn py_on_signal(&mut self, signal: &Signal) {}
1419
1420    #[allow(unused_variables, clippy::needless_pass_by_value)]
1421    #[pyo3(name = "on_instrument")]
1422    fn py_on_instrument(&mut self, instrument: Py<PyAny>) {}
1423
1424    #[allow(unused_variables)]
1425    #[pyo3(name = "on_quote")]
1426    fn py_on_quote(&mut self, quote: QuoteTick) {}
1427
1428    #[allow(unused_variables)]
1429    #[pyo3(name = "on_trade")]
1430    fn py_on_trade(&mut self, trade: TradeTick) {}
1431
1432    #[allow(unused_variables)]
1433    #[pyo3(name = "on_bar")]
1434    fn py_on_bar(&mut self, bar: Bar) {}
1435
1436    #[allow(unused_variables, clippy::needless_pass_by_value)]
1437    #[pyo3(name = "on_book_deltas")]
1438    fn py_on_book_deltas(&mut self, deltas: OrderBookDeltas) {}
1439
1440    #[allow(unused_variables)]
1441    #[pyo3(name = "on_book")]
1442    fn py_on_book(&mut self, book: &OrderBook) {}
1443
1444    #[allow(unused_variables)]
1445    #[pyo3(name = "on_mark_price")]
1446    fn py_on_mark_price(&mut self, mark_price: MarkPriceUpdate) {}
1447
1448    #[allow(unused_variables)]
1449    #[pyo3(name = "on_index_price")]
1450    fn py_on_index_price(&mut self, index_price: IndexPriceUpdate) {}
1451
1452    #[allow(unused_variables)]
1453    #[pyo3(name = "on_funding_rate")]
1454    fn py_on_funding_rate(&mut self, funding_rate: FundingRateUpdate) {}
1455
1456    #[allow(unused_variables)]
1457    #[pyo3(name = "on_instrument_status")]
1458    fn py_on_instrument_status(&mut self, status: InstrumentStatus) {}
1459
1460    #[allow(unused_variables)]
1461    #[pyo3(name = "on_instrument_close")]
1462    fn py_on_instrument_close(&mut self, close: InstrumentClose) {}
1463
1464    #[allow(unused_variables)]
1465    #[pyo3(name = "on_option_greeks")]
1466    fn py_on_option_greeks(&mut self, greeks: OptionGreeks) {}
1467
1468    #[allow(unused_variables, clippy::needless_pass_by_value)]
1469    #[pyo3(name = "on_option_chain")]
1470    fn py_on_option_chain(&mut self, slice: OptionChainSlice) {}
1471
1472    #[pyo3(name = "subscribe_data")]
1473    #[pyo3(signature = (data_type, client_id=None, params=None))]
1474    fn py_subscribe_data(
1475        &mut self,
1476        py: Python<'_>,
1477        data_type: DataType,
1478        client_id: Option<ClientId>,
1479        params: Option<Py<PyDict>>,
1480    ) -> PyResult<()> {
1481        let params = dict_to_params(py, params)?;
1482        DataActor::subscribe_data(self.inner_mut(), data_type, client_id, params);
1483        Ok(())
1484    }
1485
1486    #[pyo3(name = "subscribe_signal")]
1487    #[pyo3(signature = (name="", priority=None))]
1488    fn py_subscribe_signal(&mut self, name: &str, priority: Option<u32>) {
1489        DataActor::subscribe_signal(self.inner_mut(), name, priority);
1490    }
1491
1492    #[pyo3(name = "subscribe_instruments")]
1493    #[pyo3(signature = (venue, client_id=None, params=None))]
1494    fn py_subscribe_instruments(
1495        &mut self,
1496        py: Python<'_>,
1497        venue: Venue,
1498        client_id: Option<ClientId>,
1499        params: Option<Py<PyDict>>,
1500    ) -> PyResult<()> {
1501        let params = dict_to_params(py, params)?;
1502        DataActor::subscribe_instruments(self.inner_mut(), venue, client_id, params);
1503        Ok(())
1504    }
1505
1506    #[pyo3(name = "subscribe_instrument")]
1507    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1508    fn py_subscribe_instrument(
1509        &mut self,
1510        py: Python<'_>,
1511        instrument_id: InstrumentId,
1512        client_id: Option<ClientId>,
1513        params: Option<Py<PyDict>>,
1514    ) -> PyResult<()> {
1515        let params = dict_to_params(py, params)?;
1516        DataActor::subscribe_instrument(self.inner_mut(), instrument_id, client_id, params);
1517        Ok(())
1518    }
1519
1520    #[pyo3(name = "subscribe_book_deltas")]
1521    #[pyo3(signature = (instrument_id, book_type, depth=None, client_id=None, managed=false, params=None))]
1522    #[expect(clippy::too_many_arguments)]
1523    fn py_subscribe_book_deltas(
1524        &mut self,
1525        py: Python<'_>,
1526        instrument_id: InstrumentId,
1527        book_type: BookType,
1528        depth: Option<usize>,
1529        client_id: Option<ClientId>,
1530        managed: bool,
1531        params: Option<Py<PyDict>>,
1532    ) -> PyResult<()> {
1533        let params = dict_to_params(py, params)?;
1534        let depth = depth.and_then(NonZeroUsize::new);
1535        DataActor::subscribe_book_deltas(
1536            self.inner_mut(),
1537            instrument_id,
1538            book_type,
1539            depth,
1540            client_id,
1541            managed,
1542            params,
1543        );
1544        Ok(())
1545    }
1546
1547    #[pyo3(name = "subscribe_book_at_interval")]
1548    #[pyo3(signature = (instrument_id, book_type, interval_ms, depth=None, client_id=None, params=None))]
1549    #[expect(clippy::too_many_arguments)]
1550    fn py_subscribe_book_at_interval(
1551        &mut self,
1552        py: Python<'_>,
1553        instrument_id: InstrumentId,
1554        book_type: BookType,
1555        interval_ms: usize,
1556        depth: Option<usize>,
1557        client_id: Option<ClientId>,
1558        params: Option<Py<PyDict>>,
1559    ) -> PyResult<()> {
1560        let params = dict_to_params(py, params)?;
1561        let depth = depth.and_then(NonZeroUsize::new);
1562        let interval_ms = NonZeroUsize::new(interval_ms)
1563            .ok_or_else(|| to_pyvalue_err("interval_ms must be > 0"))?;
1564
1565        DataActor::subscribe_book_at_interval(
1566            self.inner_mut(),
1567            instrument_id,
1568            book_type,
1569            depth,
1570            interval_ms,
1571            client_id,
1572            params,
1573        );
1574        Ok(())
1575    }
1576
1577    #[pyo3(name = "subscribe_quotes")]
1578    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1579    fn py_subscribe_quotes(
1580        &mut self,
1581        py: Python<'_>,
1582        instrument_id: InstrumentId,
1583        client_id: Option<ClientId>,
1584        params: Option<Py<PyDict>>,
1585    ) -> PyResult<()> {
1586        let params = dict_to_params(py, params)?;
1587        DataActor::subscribe_quotes(self.inner_mut(), instrument_id, client_id, params);
1588        Ok(())
1589    }
1590
1591    #[pyo3(name = "subscribe_trades")]
1592    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1593    fn py_subscribe_trades(
1594        &mut self,
1595        py: Python<'_>,
1596        instrument_id: InstrumentId,
1597        client_id: Option<ClientId>,
1598        params: Option<Py<PyDict>>,
1599    ) -> PyResult<()> {
1600        let params = dict_to_params(py, params)?;
1601        DataActor::subscribe_trades(self.inner_mut(), instrument_id, client_id, params);
1602        Ok(())
1603    }
1604
1605    #[pyo3(name = "subscribe_bars")]
1606    #[pyo3(signature = (bar_type, client_id=None, params=None))]
1607    fn py_subscribe_bars(
1608        &mut self,
1609        py: Python<'_>,
1610        bar_type: BarType,
1611        client_id: Option<ClientId>,
1612        params: Option<Py<PyDict>>,
1613    ) -> PyResult<()> {
1614        let params = dict_to_params(py, params)?;
1615        DataActor::subscribe_bars(self.inner_mut(), bar_type, client_id, params);
1616        Ok(())
1617    }
1618
1619    #[pyo3(name = "subscribe_mark_prices")]
1620    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1621    fn py_subscribe_mark_prices(
1622        &mut self,
1623        py: Python<'_>,
1624        instrument_id: InstrumentId,
1625        client_id: Option<ClientId>,
1626        params: Option<Py<PyDict>>,
1627    ) -> PyResult<()> {
1628        let params = dict_to_params(py, params)?;
1629        DataActor::subscribe_mark_prices(self.inner_mut(), instrument_id, client_id, params);
1630        Ok(())
1631    }
1632
1633    #[pyo3(name = "subscribe_index_prices")]
1634    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1635    fn py_subscribe_index_prices(
1636        &mut self,
1637        py: Python<'_>,
1638        instrument_id: InstrumentId,
1639        client_id: Option<ClientId>,
1640        params: Option<Py<PyDict>>,
1641    ) -> PyResult<()> {
1642        let params = dict_to_params(py, params)?;
1643        DataActor::subscribe_index_prices(self.inner_mut(), instrument_id, client_id, params);
1644        Ok(())
1645    }
1646
1647    #[pyo3(name = "subscribe_funding_rates")]
1648    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1649    fn py_subscribe_funding_rates(
1650        &mut self,
1651        py: Python<'_>,
1652        instrument_id: InstrumentId,
1653        client_id: Option<ClientId>,
1654        params: Option<Py<PyDict>>,
1655    ) -> PyResult<()> {
1656        let params = dict_to_params(py, params)?;
1657        DataActor::subscribe_funding_rates(self.inner_mut(), instrument_id, client_id, params);
1658        Ok(())
1659    }
1660
1661    #[pyo3(name = "subscribe_option_greeks")]
1662    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1663    fn py_subscribe_option_greeks(
1664        &mut self,
1665        py: Python<'_>,
1666        instrument_id: InstrumentId,
1667        client_id: Option<ClientId>,
1668        params: Option<Py<PyDict>>,
1669    ) -> PyResult<()> {
1670        let params = dict_to_params(py, params)?;
1671        DataActor::subscribe_option_greeks(self.inner_mut(), instrument_id, client_id, params);
1672        Ok(())
1673    }
1674
1675    #[pyo3(name = "subscribe_instrument_status")]
1676    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1677    fn py_subscribe_instrument_status(
1678        &mut self,
1679        py: Python<'_>,
1680        instrument_id: InstrumentId,
1681        client_id: Option<ClientId>,
1682        params: Option<Py<PyDict>>,
1683    ) -> PyResult<()> {
1684        let params = dict_to_params(py, params)?;
1685        DataActor::subscribe_instrument_status(self.inner_mut(), instrument_id, client_id, params);
1686        Ok(())
1687    }
1688
1689    #[pyo3(name = "subscribe_instrument_close")]
1690    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1691    fn py_subscribe_instrument_close(
1692        &mut self,
1693        py: Python<'_>,
1694        instrument_id: InstrumentId,
1695        client_id: Option<ClientId>,
1696        params: Option<Py<PyDict>>,
1697    ) -> PyResult<()> {
1698        let params = dict_to_params(py, params)?;
1699        DataActor::subscribe_instrument_close(self.inner_mut(), instrument_id, client_id, params);
1700        Ok(())
1701    }
1702
1703    #[pyo3(name = "subscribe_option_chain")]
1704    #[pyo3(signature = (series_id, strike_range, snapshot_interval_ms=None, client_id=None, params=None))]
1705    fn py_subscribe_option_chain(
1706        &mut self,
1707        py: Python<'_>,
1708        series_id: OptionSeriesId,
1709        strike_range: PyStrikeRange,
1710        snapshot_interval_ms: Option<u64>,
1711        client_id: Option<ClientId>,
1712        params: Option<Py<PyDict>>,
1713    ) -> PyResult<()> {
1714        let params = dict_to_params(py, params)?;
1715        DataActor::subscribe_option_chain(
1716            self.inner_mut(),
1717            series_id,
1718            strike_range.inner,
1719            snapshot_interval_ms,
1720            client_id,
1721            params,
1722        );
1723        Ok(())
1724    }
1725
1726    #[pyo3(name = "subscribe_order_fills")]
1727    #[pyo3(signature = (instrument_id))]
1728    fn py_subscribe_order_fills(&mut self, instrument_id: InstrumentId) {
1729        DataActor::subscribe_order_fills(self.inner_mut(), instrument_id);
1730    }
1731
1732    #[pyo3(name = "subscribe_order_cancels")]
1733    #[pyo3(signature = (instrument_id))]
1734    fn py_subscribe_order_cancels(&mut self, instrument_id: InstrumentId) {
1735        DataActor::subscribe_order_cancels(self.inner_mut(), instrument_id);
1736    }
1737
1738    #[pyo3(name = "unsubscribe_data")]
1739    #[pyo3(signature = (data_type, client_id=None, params=None))]
1740    fn py_unsubscribe_data(
1741        &mut self,
1742        py: Python<'_>,
1743        data_type: DataType,
1744        client_id: Option<ClientId>,
1745        params: Option<Py<PyDict>>,
1746    ) -> PyResult<()> {
1747        let params = dict_to_params(py, params)?;
1748        DataActor::unsubscribe_data(self.inner_mut(), data_type, client_id, params);
1749        Ok(())
1750    }
1751
1752    #[pyo3(name = "unsubscribe_signal")]
1753    #[pyo3(signature = (name=""))]
1754    fn py_unsubscribe_signal(&mut self, name: &str) {
1755        DataActor::unsubscribe_signal(self.inner_mut(), name);
1756    }
1757
1758    #[pyo3(name = "unsubscribe_instruments")]
1759    #[pyo3(signature = (venue, client_id=None, params=None))]
1760    fn py_unsubscribe_instruments(
1761        &mut self,
1762        py: Python<'_>,
1763        venue: Venue,
1764        client_id: Option<ClientId>,
1765        params: Option<Py<PyDict>>,
1766    ) -> PyResult<()> {
1767        let params = dict_to_params(py, params)?;
1768        DataActor::unsubscribe_instruments(self.inner_mut(), venue, client_id, params);
1769        Ok(())
1770    }
1771
1772    #[pyo3(name = "unsubscribe_instrument")]
1773    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1774    fn py_unsubscribe_instrument(
1775        &mut self,
1776        py: Python<'_>,
1777        instrument_id: InstrumentId,
1778        client_id: Option<ClientId>,
1779        params: Option<Py<PyDict>>,
1780    ) -> PyResult<()> {
1781        let params = dict_to_params(py, params)?;
1782        DataActor::unsubscribe_instrument(self.inner_mut(), instrument_id, client_id, params);
1783        Ok(())
1784    }
1785
1786    #[pyo3(name = "unsubscribe_book_deltas")]
1787    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1788    fn py_unsubscribe_book_deltas(
1789        &mut self,
1790        py: Python<'_>,
1791        instrument_id: InstrumentId,
1792        client_id: Option<ClientId>,
1793        params: Option<Py<PyDict>>,
1794    ) -> PyResult<()> {
1795        let params = dict_to_params(py, params)?;
1796        DataActor::unsubscribe_book_deltas(self.inner_mut(), instrument_id, client_id, params);
1797        Ok(())
1798    }
1799
1800    #[pyo3(name = "unsubscribe_book_at_interval")]
1801    #[pyo3(signature = (instrument_id, interval_ms, client_id=None, params=None))]
1802    fn py_unsubscribe_book_at_interval(
1803        &mut self,
1804        py: Python<'_>,
1805        instrument_id: InstrumentId,
1806        interval_ms: usize,
1807        client_id: Option<ClientId>,
1808        params: Option<Py<PyDict>>,
1809    ) -> PyResult<()> {
1810        let params = dict_to_params(py, params)?;
1811        let interval_ms = NonZeroUsize::new(interval_ms)
1812            .ok_or_else(|| to_pyvalue_err("interval_ms must be > 0"))?;
1813
1814        DataActor::unsubscribe_book_at_interval(
1815            self.inner_mut(),
1816            instrument_id,
1817            interval_ms,
1818            client_id,
1819            params,
1820        );
1821        Ok(())
1822    }
1823
1824    #[pyo3(name = "unsubscribe_quotes")]
1825    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1826    fn py_unsubscribe_quotes(
1827        &mut self,
1828        py: Python<'_>,
1829        instrument_id: InstrumentId,
1830        client_id: Option<ClientId>,
1831        params: Option<Py<PyDict>>,
1832    ) -> PyResult<()> {
1833        let params = dict_to_params(py, params)?;
1834        DataActor::unsubscribe_quotes(self.inner_mut(), instrument_id, client_id, params);
1835        Ok(())
1836    }
1837
1838    #[pyo3(name = "unsubscribe_trades")]
1839    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1840    fn py_unsubscribe_trades(
1841        &mut self,
1842        py: Python<'_>,
1843        instrument_id: InstrumentId,
1844        client_id: Option<ClientId>,
1845        params: Option<Py<PyDict>>,
1846    ) -> PyResult<()> {
1847        let params = dict_to_params(py, params)?;
1848        DataActor::unsubscribe_trades(self.inner_mut(), instrument_id, client_id, params);
1849        Ok(())
1850    }
1851
1852    #[pyo3(name = "unsubscribe_bars")]
1853    #[pyo3(signature = (bar_type, client_id=None, params=None))]
1854    fn py_unsubscribe_bars(
1855        &mut self,
1856        py: Python<'_>,
1857        bar_type: BarType,
1858        client_id: Option<ClientId>,
1859        params: Option<Py<PyDict>>,
1860    ) -> PyResult<()> {
1861        let params = dict_to_params(py, params)?;
1862        DataActor::unsubscribe_bars(self.inner_mut(), bar_type, client_id, params);
1863        Ok(())
1864    }
1865
1866    #[pyo3(name = "unsubscribe_mark_prices")]
1867    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1868    fn py_unsubscribe_mark_prices(
1869        &mut self,
1870        py: Python<'_>,
1871        instrument_id: InstrumentId,
1872        client_id: Option<ClientId>,
1873        params: Option<Py<PyDict>>,
1874    ) -> PyResult<()> {
1875        let params = dict_to_params(py, params)?;
1876        DataActor::unsubscribe_mark_prices(self.inner_mut(), instrument_id, client_id, params);
1877        Ok(())
1878    }
1879
1880    #[pyo3(name = "unsubscribe_index_prices")]
1881    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1882    fn py_unsubscribe_index_prices(
1883        &mut self,
1884        py: Python<'_>,
1885        instrument_id: InstrumentId,
1886        client_id: Option<ClientId>,
1887        params: Option<Py<PyDict>>,
1888    ) -> PyResult<()> {
1889        let params = dict_to_params(py, params)?;
1890        DataActor::unsubscribe_index_prices(self.inner_mut(), instrument_id, client_id, params);
1891        Ok(())
1892    }
1893
1894    #[pyo3(name = "unsubscribe_funding_rates")]
1895    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1896    fn py_unsubscribe_funding_rates(
1897        &mut self,
1898        py: Python<'_>,
1899        instrument_id: InstrumentId,
1900        client_id: Option<ClientId>,
1901        params: Option<Py<PyDict>>,
1902    ) -> PyResult<()> {
1903        let params = dict_to_params(py, params)?;
1904        DataActor::unsubscribe_funding_rates(self.inner_mut(), instrument_id, client_id, params);
1905        Ok(())
1906    }
1907
1908    #[pyo3(name = "unsubscribe_option_greeks")]
1909    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1910    fn py_unsubscribe_option_greeks(
1911        &mut self,
1912        py: Python<'_>,
1913        instrument_id: InstrumentId,
1914        client_id: Option<ClientId>,
1915        params: Option<Py<PyDict>>,
1916    ) -> PyResult<()> {
1917        let params = dict_to_params(py, params)?;
1918        DataActor::unsubscribe_option_greeks(self.inner_mut(), instrument_id, client_id, params);
1919        Ok(())
1920    }
1921
1922    #[pyo3(name = "unsubscribe_instrument_status")]
1923    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1924    fn py_unsubscribe_instrument_status(
1925        &mut self,
1926        py: Python<'_>,
1927        instrument_id: InstrumentId,
1928        client_id: Option<ClientId>,
1929        params: Option<Py<PyDict>>,
1930    ) -> PyResult<()> {
1931        let params = dict_to_params(py, params)?;
1932        DataActor::unsubscribe_instrument_status(
1933            self.inner_mut(),
1934            instrument_id,
1935            client_id,
1936            params,
1937        );
1938        Ok(())
1939    }
1940
1941    #[pyo3(name = "unsubscribe_instrument_close")]
1942    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1943    fn py_unsubscribe_instrument_close(
1944        &mut self,
1945        py: Python<'_>,
1946        instrument_id: InstrumentId,
1947        client_id: Option<ClientId>,
1948        params: Option<Py<PyDict>>,
1949    ) -> PyResult<()> {
1950        let params = dict_to_params(py, params)?;
1951        DataActor::unsubscribe_instrument_close(self.inner_mut(), instrument_id, client_id, params);
1952        Ok(())
1953    }
1954
1955    #[pyo3(name = "unsubscribe_option_chain")]
1956    #[pyo3(signature = (series_id, client_id=None))]
1957    fn py_unsubscribe_option_chain(
1958        &mut self,
1959        series_id: OptionSeriesId,
1960        client_id: Option<ClientId>,
1961    ) {
1962        DataActor::unsubscribe_option_chain(self.inner_mut(), series_id, client_id);
1963    }
1964
1965    #[pyo3(name = "unsubscribe_order_fills")]
1966    #[pyo3(signature = (instrument_id))]
1967    fn py_unsubscribe_order_fills(&mut self, instrument_id: InstrumentId) {
1968        DataActor::unsubscribe_order_fills(self.inner_mut(), instrument_id);
1969    }
1970
1971    #[pyo3(name = "unsubscribe_order_cancels")]
1972    #[pyo3(signature = (instrument_id))]
1973    fn py_unsubscribe_order_cancels(&mut self, instrument_id: InstrumentId) {
1974        DataActor::unsubscribe_order_cancels(self.inner_mut(), instrument_id);
1975    }
1976
1977    #[pyo3(name = "request_data")]
1978    #[pyo3(signature = (data_type, client_id, start=None, end=None, limit=None, params=None))]
1979    #[expect(clippy::too_many_arguments)]
1980    fn py_request_data(
1981        &mut self,
1982        py: Python<'_>,
1983        data_type: DataType,
1984        client_id: ClientId,
1985        start: Option<DateTime<Utc>>,
1986        end: Option<DateTime<Utc>>,
1987        limit: Option<usize>,
1988        params: Option<Py<PyDict>>,
1989    ) -> PyResult<String> {
1990        let params = dict_to_params(py, params)?;
1991        let limit = limit.and_then(NonZeroUsize::new);
1992        let request_id = DataActor::request_data(
1993            self.inner_mut(),
1994            data_type,
1995            client_id,
1996            start,
1997            end,
1998            limit,
1999            params,
2000        )
2001        .map_err(to_pyvalue_err)?;
2002        Ok(request_id.to_string())
2003    }
2004
2005    #[pyo3(name = "request_instrument")]
2006    #[pyo3(signature = (instrument_id, start=None, end=None, client_id=None, params=None))]
2007    fn py_request_instrument(
2008        &mut self,
2009        py: Python<'_>,
2010        instrument_id: InstrumentId,
2011        start: Option<DateTime<Utc>>,
2012        end: Option<DateTime<Utc>>,
2013        client_id: Option<ClientId>,
2014        params: Option<Py<PyDict>>,
2015    ) -> PyResult<String> {
2016        let params = dict_to_params(py, params)?;
2017        let request_id = DataActor::request_instrument(
2018            self.inner_mut(),
2019            instrument_id,
2020            start,
2021            end,
2022            client_id,
2023            params,
2024        )
2025        .map_err(to_pyvalue_err)?;
2026        Ok(request_id.to_string())
2027    }
2028
2029    #[pyo3(name = "request_instruments")]
2030    #[pyo3(signature = (venue=None, start=None, end=None, client_id=None, params=None))]
2031    fn py_request_instruments(
2032        &mut self,
2033        py: Python<'_>,
2034        venue: Option<Venue>,
2035        start: Option<DateTime<Utc>>,
2036        end: Option<DateTime<Utc>>,
2037        client_id: Option<ClientId>,
2038        params: Option<Py<PyDict>>,
2039    ) -> PyResult<String> {
2040        let params = dict_to_params(py, params)?;
2041        let request_id =
2042            DataActor::request_instruments(self.inner_mut(), venue, start, end, client_id, params)
2043                .map_err(to_pyvalue_err)?;
2044        Ok(request_id.to_string())
2045    }
2046
2047    #[pyo3(name = "request_book_snapshot")]
2048    #[pyo3(signature = (instrument_id, depth=None, client_id=None, params=None))]
2049    fn py_request_book_snapshot(
2050        &mut self,
2051        py: Python<'_>,
2052        instrument_id: InstrumentId,
2053        depth: Option<usize>,
2054        client_id: Option<ClientId>,
2055        params: Option<Py<PyDict>>,
2056    ) -> PyResult<String> {
2057        let params = dict_to_params(py, params)?;
2058        let depth = depth.and_then(NonZeroUsize::new);
2059
2060        let request_id = DataActor::request_book_snapshot(
2061            self.inner_mut(),
2062            instrument_id,
2063            depth,
2064            client_id,
2065            params,
2066        )
2067        .map_err(to_pyvalue_err)?;
2068        Ok(request_id.to_string())
2069    }
2070
2071    #[pyo3(name = "request_book_deltas")]
2072    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None))]
2073    #[expect(clippy::too_many_arguments)]
2074    fn py_request_book_deltas(
2075        &mut self,
2076        py: Python<'_>,
2077        instrument_id: InstrumentId,
2078        start: Option<DateTime<Utc>>,
2079        end: Option<DateTime<Utc>>,
2080        limit: Option<usize>,
2081        client_id: Option<ClientId>,
2082        params: Option<Py<PyDict>>,
2083    ) -> PyResult<String> {
2084        let params = dict_to_params(py, params)?;
2085        let limit = limit.and_then(NonZeroUsize::new);
2086        let request_id = DataActor::request_book_deltas(
2087            self.inner_mut(),
2088            instrument_id,
2089            start,
2090            end,
2091            limit,
2092            client_id,
2093            params,
2094        )
2095        .map_err(to_pyvalue_err)?;
2096        Ok(request_id.to_string())
2097    }
2098
2099    #[pyo3(name = "request_book_depth")]
2100    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, depth=None, client_id=None, params=None))]
2101    #[expect(clippy::too_many_arguments)]
2102    fn py_request_book_depth(
2103        &mut self,
2104        py: Python<'_>,
2105        instrument_id: InstrumentId,
2106        start: Option<DateTime<Utc>>,
2107        end: Option<DateTime<Utc>>,
2108        limit: Option<usize>,
2109        depth: Option<usize>,
2110        client_id: Option<ClientId>,
2111        params: Option<Py<PyDict>>,
2112    ) -> PyResult<String> {
2113        let params = dict_to_params(py, params)?;
2114        let limit = limit.and_then(NonZeroUsize::new);
2115        let depth = depth.and_then(NonZeroUsize::new);
2116        let request_id = DataActor::request_book_depth(
2117            self.inner_mut(),
2118            instrument_id,
2119            start,
2120            end,
2121            limit,
2122            depth,
2123            client_id,
2124            params,
2125        )
2126        .map_err(to_pyvalue_err)?;
2127        Ok(request_id.to_string())
2128    }
2129
2130    #[pyo3(name = "request_quotes")]
2131    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None))]
2132    #[expect(clippy::too_many_arguments)]
2133    fn py_request_quotes(
2134        &mut self,
2135        py: Python<'_>,
2136        instrument_id: InstrumentId,
2137        start: Option<DateTime<Utc>>,
2138        end: Option<DateTime<Utc>>,
2139        limit: Option<usize>,
2140        client_id: Option<ClientId>,
2141        params: Option<Py<PyDict>>,
2142    ) -> PyResult<String> {
2143        let params = dict_to_params(py, params)?;
2144        let limit = limit.and_then(NonZeroUsize::new);
2145        let request_id = DataActor::request_quotes(
2146            self.inner_mut(),
2147            instrument_id,
2148            start,
2149            end,
2150            limit,
2151            client_id,
2152            params,
2153        )
2154        .map_err(to_pyvalue_err)?;
2155        Ok(request_id.to_string())
2156    }
2157
2158    #[pyo3(name = "request_trades")]
2159    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None))]
2160    #[expect(clippy::too_many_arguments)]
2161    fn py_request_trades(
2162        &mut self,
2163        py: Python<'_>,
2164        instrument_id: InstrumentId,
2165        start: Option<DateTime<Utc>>,
2166        end: Option<DateTime<Utc>>,
2167        limit: Option<usize>,
2168        client_id: Option<ClientId>,
2169        params: Option<Py<PyDict>>,
2170    ) -> PyResult<String> {
2171        let params = dict_to_params(py, params)?;
2172        let limit = limit.and_then(NonZeroUsize::new);
2173        let request_id = DataActor::request_trades(
2174            self.inner_mut(),
2175            instrument_id,
2176            start,
2177            end,
2178            limit,
2179            client_id,
2180            params,
2181        )
2182        .map_err(to_pyvalue_err)?;
2183        Ok(request_id.to_string())
2184    }
2185
2186    #[pyo3(name = "request_funding_rates")]
2187    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None))]
2188    #[expect(clippy::too_many_arguments)]
2189    fn py_request_funding_rates(
2190        &mut self,
2191        py: Python<'_>,
2192        instrument_id: InstrumentId,
2193        start: Option<DateTime<Utc>>,
2194        end: Option<DateTime<Utc>>,
2195        limit: Option<usize>,
2196        client_id: Option<ClientId>,
2197        params: Option<Py<PyDict>>,
2198    ) -> PyResult<String> {
2199        let params = dict_to_params(py, params)?;
2200        let limit = limit.and_then(NonZeroUsize::new);
2201        let request_id = DataActor::request_funding_rates(
2202            self.inner_mut(),
2203            instrument_id,
2204            start,
2205            end,
2206            limit,
2207            client_id,
2208            params,
2209        )
2210        .map_err(to_pyvalue_err)?;
2211        Ok(request_id.to_string())
2212    }
2213
2214    #[pyo3(name = "request_bars")]
2215    #[pyo3(signature = (bar_type, start=None, end=None, limit=None, client_id=None, params=None))]
2216    #[expect(clippy::too_many_arguments)]
2217    fn py_request_bars(
2218        &mut self,
2219        py: Python<'_>,
2220        bar_type: BarType,
2221        start: Option<DateTime<Utc>>,
2222        end: Option<DateTime<Utc>>,
2223        limit: Option<usize>,
2224        client_id: Option<ClientId>,
2225        params: Option<Py<PyDict>>,
2226    ) -> PyResult<String> {
2227        let params = dict_to_params(py, params)?;
2228        let limit = limit.and_then(NonZeroUsize::new);
2229        let request_id = DataActor::request_bars(
2230            self.inner_mut(),
2231            bar_type,
2232            start,
2233            end,
2234            limit,
2235            client_id,
2236            params,
2237        )
2238        .map_err(to_pyvalue_err)?;
2239        Ok(request_id.to_string())
2240    }
2241
2242    #[allow(unused_variables, clippy::needless_pass_by_value)]
2243    #[pyo3(name = "on_historical_data")]
2244    fn py_on_historical_data(&mut self, data: Py<PyAny>) {
2245        // Default implementation - can be overridden in Python subclasses
2246    }
2247
2248    #[allow(unused_variables, clippy::needless_pass_by_value)]
2249    #[pyo3(name = "on_historical_quotes")]
2250    fn py_on_historical_quotes(&mut self, quotes: Vec<QuoteTick>) {
2251        // Default implementation - can be overridden in Python subclasses
2252    }
2253
2254    #[allow(unused_variables, clippy::needless_pass_by_value)]
2255    #[pyo3(name = "on_historical_trades")]
2256    fn py_on_historical_trades(&mut self, trades: Vec<TradeTick>) {
2257        // Default implementation - can be overridden in Python subclasses
2258    }
2259
2260    #[allow(unused_variables, clippy::needless_pass_by_value)]
2261    #[pyo3(name = "on_historical_funding_rates")]
2262    fn py_on_historical_funding_rates(&mut self, funding_rates: Vec<FundingRateUpdate>) {
2263        // Default implementation - can be overridden in Python subclasses
2264    }
2265
2266    #[allow(unused_variables, clippy::needless_pass_by_value)]
2267    #[pyo3(name = "on_historical_bars")]
2268    fn py_on_historical_bars(&mut self, bars: Vec<Bar>) {
2269        // Default implementation - can be overridden in Python subclasses
2270    }
2271
2272    #[allow(unused_variables, clippy::needless_pass_by_value)]
2273    #[pyo3(name = "on_historical_mark_prices")]
2274    fn py_on_historical_mark_prices(&mut self, mark_prices: Vec<MarkPriceUpdate>) {
2275        // Default implementation - can be overridden in Python subclasses
2276    }
2277
2278    #[allow(unused_variables, clippy::needless_pass_by_value)]
2279    #[pyo3(name = "on_historical_index_prices")]
2280    fn py_on_historical_index_prices(&mut self, index_prices: Vec<IndexPriceUpdate>) {
2281        // Default implementation - can be overridden in Python subclasses
2282    }
2283}
2284
2285#[cfg(feature = "defi")]
2286#[pymethods]
2287#[pyo3_stub_gen::derive::gen_stub_pymethods]
2288impl PyDataActor {
2289    #[pyo3(name = "on_block")]
2290    #[allow(unused_variables, clippy::needless_pass_by_value)]
2291    fn py_on_block(&mut self, block: Block) {}
2292
2293    #[pyo3(name = "on_pool")]
2294    #[allow(unused_variables, clippy::needless_pass_by_value)]
2295    fn py_on_pool(&mut self, pool: Pool) {}
2296
2297    #[pyo3(name = "on_pool_swap")]
2298    #[allow(unused_variables, clippy::needless_pass_by_value)]
2299    fn py_on_pool_swap(&mut self, swap: PoolSwap) {}
2300
2301    #[pyo3(name = "on_pool_liquidity_update")]
2302    #[allow(unused_variables, clippy::needless_pass_by_value)]
2303    fn py_on_pool_liquidity_update(&mut self, update: PoolLiquidityUpdate) {}
2304
2305    #[pyo3(name = "on_pool_fee_collect")]
2306    #[allow(unused_variables, clippy::needless_pass_by_value)]
2307    fn py_on_pool_fee_collect(&mut self, update: PoolFeeCollect) {}
2308
2309    #[pyo3(name = "on_pool_flash")]
2310    #[allow(unused_variables, clippy::needless_pass_by_value)]
2311    fn py_on_pool_flash(&mut self, flash: PoolFlash) {}
2312
2313    #[pyo3(name = "subscribe_blocks")]
2314    #[pyo3(signature = (chain, client_id=None, params=None))]
2315    fn py_subscribe_blocks(
2316        &mut self,
2317        py: Python<'_>,
2318        chain: Blockchain,
2319        client_id: Option<ClientId>,
2320        params: Option<Py<PyDict>>,
2321    ) -> PyResult<()> {
2322        let params = dict_to_params(py, params)?;
2323        DataActor::subscribe_blocks(self.inner_mut(), chain, client_id, params);
2324        Ok(())
2325    }
2326
2327    #[pyo3(name = "subscribe_pool")]
2328    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2329    fn py_subscribe_pool(
2330        &mut self,
2331        py: Python<'_>,
2332        instrument_id: InstrumentId,
2333        client_id: Option<ClientId>,
2334        params: Option<Py<PyDict>>,
2335    ) -> PyResult<()> {
2336        let params = dict_to_params(py, params)?;
2337        DataActor::subscribe_pool(self.inner_mut(), instrument_id, client_id, params);
2338        Ok(())
2339    }
2340
2341    #[pyo3(name = "subscribe_pool_swaps")]
2342    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2343    fn py_subscribe_pool_swaps(
2344        &mut self,
2345        py: Python<'_>,
2346        instrument_id: InstrumentId,
2347        client_id: Option<ClientId>,
2348        params: Option<Py<PyDict>>,
2349    ) -> PyResult<()> {
2350        let params = dict_to_params(py, params)?;
2351        DataActor::subscribe_pool_swaps(self.inner_mut(), instrument_id, client_id, params);
2352        Ok(())
2353    }
2354
2355    #[pyo3(name = "subscribe_pool_liquidity_updates")]
2356    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2357    fn py_subscribe_pool_liquidity_updates(
2358        &mut self,
2359        py: Python<'_>,
2360        instrument_id: InstrumentId,
2361        client_id: Option<ClientId>,
2362        params: Option<Py<PyDict>>,
2363    ) -> PyResult<()> {
2364        let params = dict_to_params(py, params)?;
2365        DataActor::subscribe_pool_liquidity_updates(
2366            self.inner_mut(),
2367            instrument_id,
2368            client_id,
2369            params,
2370        );
2371        Ok(())
2372    }
2373
2374    #[pyo3(name = "subscribe_pool_fee_collects")]
2375    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2376    fn py_subscribe_pool_fee_collects(
2377        &mut self,
2378        py: Python<'_>,
2379        instrument_id: InstrumentId,
2380        client_id: Option<ClientId>,
2381        params: Option<Py<PyDict>>,
2382    ) -> PyResult<()> {
2383        let params = dict_to_params(py, params)?;
2384        DataActor::subscribe_pool_fee_collects(self.inner_mut(), instrument_id, client_id, params);
2385        Ok(())
2386    }
2387
2388    #[pyo3(name = "subscribe_pool_flash_events")]
2389    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2390    fn py_subscribe_pool_flash_events(
2391        &mut self,
2392        py: Python<'_>,
2393        instrument_id: InstrumentId,
2394        client_id: Option<ClientId>,
2395        params: Option<Py<PyDict>>,
2396    ) -> PyResult<()> {
2397        let params = dict_to_params(py, params)?;
2398        DataActor::subscribe_pool_flash_events(self.inner_mut(), instrument_id, client_id, params);
2399        Ok(())
2400    }
2401
2402    #[pyo3(name = "unsubscribe_blocks")]
2403    #[pyo3(signature = (chain, client_id=None, params=None))]
2404    fn py_unsubscribe_blocks(
2405        &mut self,
2406        py: Python<'_>,
2407        chain: Blockchain,
2408        client_id: Option<ClientId>,
2409        params: Option<Py<PyDict>>,
2410    ) -> PyResult<()> {
2411        let params = dict_to_params(py, params)?;
2412        DataActor::unsubscribe_blocks(self.inner_mut(), chain, client_id, params);
2413        Ok(())
2414    }
2415
2416    #[pyo3(name = "unsubscribe_pool")]
2417    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2418    fn py_unsubscribe_pool(
2419        &mut self,
2420        py: Python<'_>,
2421        instrument_id: InstrumentId,
2422        client_id: Option<ClientId>,
2423        params: Option<Py<PyDict>>,
2424    ) -> PyResult<()> {
2425        let params = dict_to_params(py, params)?;
2426        DataActor::unsubscribe_pool(self.inner_mut(), instrument_id, client_id, params);
2427        Ok(())
2428    }
2429
2430    #[pyo3(name = "unsubscribe_pool_swaps")]
2431    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2432    fn py_unsubscribe_pool_swaps(
2433        &mut self,
2434        py: Python<'_>,
2435        instrument_id: InstrumentId,
2436        client_id: Option<ClientId>,
2437        params: Option<Py<PyDict>>,
2438    ) -> PyResult<()> {
2439        let params = dict_to_params(py, params)?;
2440        DataActor::unsubscribe_pool_swaps(self.inner_mut(), instrument_id, client_id, params);
2441        Ok(())
2442    }
2443
2444    #[pyo3(name = "unsubscribe_pool_liquidity_updates")]
2445    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2446    fn py_unsubscribe_pool_liquidity_updates(
2447        &mut self,
2448        py: Python<'_>,
2449        instrument_id: InstrumentId,
2450        client_id: Option<ClientId>,
2451        params: Option<Py<PyDict>>,
2452    ) -> PyResult<()> {
2453        let params = dict_to_params(py, params)?;
2454        DataActor::unsubscribe_pool_liquidity_updates(
2455            self.inner_mut(),
2456            instrument_id,
2457            client_id,
2458            params,
2459        );
2460        Ok(())
2461    }
2462
2463    #[pyo3(name = "unsubscribe_pool_fee_collects")]
2464    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2465    fn py_unsubscribe_pool_fee_collects(
2466        &mut self,
2467        py: Python<'_>,
2468        instrument_id: InstrumentId,
2469        client_id: Option<ClientId>,
2470        params: Option<Py<PyDict>>,
2471    ) -> PyResult<()> {
2472        let params = dict_to_params(py, params)?;
2473        DataActor::unsubscribe_pool_fee_collects(
2474            self.inner_mut(),
2475            instrument_id,
2476            client_id,
2477            params,
2478        );
2479        Ok(())
2480    }
2481
2482    #[pyo3(name = "unsubscribe_pool_flash_events")]
2483    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2484    fn py_unsubscribe_pool_flash_events(
2485        &mut self,
2486        py: Python<'_>,
2487        instrument_id: InstrumentId,
2488        client_id: Option<ClientId>,
2489        params: Option<Py<PyDict>>,
2490    ) -> PyResult<()> {
2491        let params = dict_to_params(py, params)?;
2492        DataActor::unsubscribe_pool_flash_events(
2493            self.inner_mut(),
2494            instrument_id,
2495            client_id,
2496            params,
2497        );
2498        Ok(())
2499    }
2500}
2501
2502#[cfg(test)]
2503mod tests {
2504    use std::{cell::RefCell, collections::HashMap, rc::Rc, str::FromStr, sync::Arc};
2505
2506    #[cfg(feature = "defi")]
2507    use alloy_primitives::{I256, U160, U256};
2508    use indexmap::IndexMap;
2509    use nautilus_core::{UUID4, UnixNanos, python::IntoPyObjectNautilusExt};
2510    #[cfg(feature = "defi")]
2511    use nautilus_model::defi::{
2512        AmmType, Block, Blockchain, Chain, Dex, DexType, Pool, PoolFeeCollect, PoolFlash,
2513        PoolIdentifier, PoolLiquidityUpdate, PoolLiquidityUpdateType, PoolSwap, Token,
2514    };
2515    use nautilus_model::{
2516        data::{
2517            Bar, BarType, CustomData, DataType, FundingRateUpdate, IndexPriceUpdate,
2518            InstrumentStatus, MarkPriceUpdate, OrderBookDelta, OrderBookDeltas, QuoteTick,
2519            TradeTick,
2520            close::InstrumentClose,
2521            greeks::OptionGreekValues,
2522            option_chain::{OptionChainSlice, OptionGreeks},
2523            stubs::stub_custom_data,
2524        },
2525        enums::{
2526            AggressorSide, BookType, GreeksConvention, InstrumentCloseType, MarketStatusAction,
2527        },
2528        identifiers::{ClientId, OptionSeriesId, TradeId, TraderId, Venue},
2529        instruments::{CurrencyPair, InstrumentAny, stubs::audusd_sim},
2530        orderbook::OrderBook,
2531        types::{Price, Quantity},
2532    };
2533    use pyo3::{
2534        Bound, Py, PyAny, PyResult, Python,
2535        ffi::c_str,
2536        types::{PyAnyMethods, PyBytes, PyDict, PyList},
2537    };
2538    use rstest::{fixture, rstest};
2539    use ustr::Ustr;
2540
2541    use super::PyDataActor;
2542    use crate::{
2543        actor::DataActor,
2544        cache::Cache,
2545        clock::TestClock,
2546        component::Component,
2547        enums::ComponentState,
2548        messages::data::{BarsResponse, QuotesResponse, TradesResponse},
2549        runner::{SyncDataCommandSender, set_data_cmd_sender},
2550        signal::Signal,
2551        timer::TimeEvent,
2552    };
2553
2554    #[fixture]
2555    fn clock() -> Rc<RefCell<TestClock>> {
2556        Rc::new(RefCell::new(TestClock::new()))
2557    }
2558
2559    #[fixture]
2560    fn cache() -> Rc<RefCell<Cache>> {
2561        Rc::new(RefCell::new(Cache::new(None, None)))
2562    }
2563
2564    #[fixture]
2565    fn trader_id() -> TraderId {
2566        TraderId::from("TRADER-001")
2567    }
2568
2569    #[fixture]
2570    fn client_id() -> ClientId {
2571        ClientId::new("TestClient")
2572    }
2573
2574    #[fixture]
2575    fn venue() -> Venue {
2576        Venue::from("SIM")
2577    }
2578
2579    #[fixture]
2580    fn data_type() -> DataType {
2581        DataType::new("TestData", None, None)
2582    }
2583
2584    #[fixture]
2585    fn bar_type(audusd_sim: CurrencyPair) -> BarType {
2586        BarType::from_str(&format!("{}-1-MINUTE-LAST-INTERNAL", audusd_sim.id)).unwrap()
2587    }
2588
2589    fn create_unregistered_actor() -> PyDataActor {
2590        PyDataActor::new(None)
2591    }
2592
2593    fn create_registered_actor(
2594        clock: Rc<RefCell<TestClock>>,
2595        cache: Rc<RefCell<Cache>>,
2596        trader_id: TraderId,
2597    ) -> PyDataActor {
2598        // Set up sync data command sender for tests
2599        let sender = SyncDataCommandSender;
2600        set_data_cmd_sender(Arc::new(sender));
2601
2602        let mut actor = PyDataActor::new(None);
2603        actor.register(trader_id, clock, cache).unwrap();
2604        actor
2605    }
2606
2607    #[rstest]
2608    fn test_new_actor_creation() {
2609        let actor = PyDataActor::new(None);
2610        assert!(actor.trader_id().is_none());
2611    }
2612
2613    #[rstest]
2614    fn test_actor_retains_python_config_object() {
2615        pyo3::Python::initialize();
2616        Python::attach(|py| {
2617            let config = py
2618                .eval(
2619                    c_str!("type('_Cfg', (), {'actor_id': 'A-RETAIN-001'})()"),
2620                    None,
2621                    None,
2622                )
2623                .unwrap();
2624
2625            let actor = py
2626                .get_type::<PyDataActor>()
2627                .as_any()
2628                .call1((config.clone(),))
2629                .unwrap();
2630
2631            let retained = actor.getattr("config").unwrap();
2632
2633            assert!(retained.is(&config));
2634        });
2635    }
2636
2637    #[rstest]
2638    fn test_clock_access_before_registration_raises_error() {
2639        let actor = PyDataActor::new(None);
2640
2641        // Accessing clock before registration should raise PyRuntimeError
2642        let result = actor.py_clock();
2643        assert!(result.is_err());
2644
2645        let error = result.unwrap_err();
2646        pyo3::Python::initialize();
2647
2648        pyo3::Python::attach(|py| {
2649            assert!(error.is_instance_of::<pyo3::exceptions::PyRuntimeError>(py));
2650        });
2651
2652        let error_msg = error.to_string();
2653        assert!(
2654            error_msg.contains("Actor must be registered with a trader before accessing clock")
2655        );
2656    }
2657
2658    #[rstest]
2659    fn test_unregistered_actor_methods_work() {
2660        let actor = create_unregistered_actor();
2661
2662        assert!(!actor.py_is_ready());
2663        assert!(!actor.py_is_running());
2664        assert!(!actor.py_is_stopped());
2665        assert!(!actor.py_is_disposed());
2666        assert!(!actor.py_is_degraded());
2667        assert!(!actor.py_is_faulted());
2668
2669        // Verify unregistered state
2670        assert_eq!(actor.trader_id(), None);
2671    }
2672
2673    #[rstest]
2674    fn test_registration_success(
2675        clock: Rc<RefCell<TestClock>>,
2676        cache: Rc<RefCell<Cache>>,
2677        trader_id: TraderId,
2678    ) {
2679        let mut actor = create_unregistered_actor();
2680        actor.register(trader_id, clock, cache).unwrap();
2681        assert!(actor.trader_id().is_some());
2682        assert_eq!(actor.trader_id().unwrap(), trader_id);
2683    }
2684
2685    #[rstest]
2686    fn test_registered_actor_basic_properties(
2687        clock: Rc<RefCell<TestClock>>,
2688        cache: Rc<RefCell<Cache>>,
2689        trader_id: TraderId,
2690    ) {
2691        let actor = create_registered_actor(clock, cache, trader_id);
2692
2693        assert_eq!(actor.state(), ComponentState::Ready);
2694        assert_eq!(actor.trader_id(), Some(TraderId::from("TRADER-001")));
2695        assert!(actor.py_is_ready());
2696        assert!(!actor.py_is_running());
2697        assert!(!actor.py_is_stopped());
2698        assert!(!actor.py_is_disposed());
2699        assert!(!actor.py_is_degraded());
2700        assert!(!actor.py_is_faulted());
2701    }
2702
2703    #[rstest]
2704    fn test_basic_subscription_methods_compile(
2705        clock: Rc<RefCell<TestClock>>,
2706        cache: Rc<RefCell<Cache>>,
2707        trader_id: TraderId,
2708        data_type: DataType,
2709        client_id: ClientId,
2710        audusd_sim: CurrencyPair,
2711    ) {
2712        let mut actor = create_registered_actor(clock, cache, trader_id);
2713
2714        pyo3::Python::initialize();
2715
2716        pyo3::Python::attach(|py| {
2717            assert!(
2718                actor
2719                    .py_subscribe_data(py, data_type.clone(), Some(client_id), None)
2720                    .is_ok()
2721            );
2722            assert!(
2723                actor
2724                    .py_subscribe_quotes(py, audusd_sim.id, Some(client_id), None)
2725                    .is_ok()
2726            );
2727            assert!(
2728                actor
2729                    .py_unsubscribe_data(py, data_type, Some(client_id), None)
2730                    .is_ok()
2731            );
2732            assert!(
2733                actor
2734                    .py_unsubscribe_quotes(py, audusd_sim.id, Some(client_id), None)
2735                    .is_ok()
2736            );
2737        });
2738    }
2739
2740    #[rstest]
2741    fn test_shutdown_system_passes_through(
2742        clock: Rc<RefCell<TestClock>>,
2743        cache: Rc<RefCell<Cache>>,
2744        trader_id: TraderId,
2745    ) {
2746        let actor = create_registered_actor(clock, cache, trader_id);
2747
2748        actor.py_shutdown_system(Some("Test shutdown".to_string()));
2749        actor.py_shutdown_system(None);
2750    }
2751
2752    #[rstest]
2753    fn test_publish_data_delivers_to_any_subscriber(
2754        clock: Rc<RefCell<TestClock>>,
2755        cache: Rc<RefCell<Cache>>,
2756        trader_id: TraderId,
2757    ) {
2758        use crate::msgbus::{
2759            self, MessageBus, get_message_bus, switchboard::get_custom_topic,
2760            typed_handler::ShareableMessageHandler,
2761        };
2762
2763        // Ensure clean msgbus for this test
2764        *get_message_bus().borrow_mut() = MessageBus::default();
2765
2766        let actor = create_registered_actor(clock, cache, trader_id);
2767        let data = stub_custom_data(1, 42, None, None);
2768        let topic = get_custom_topic(&data.data_type);
2769
2770        let received: Rc<RefCell<Vec<CustomData>>> = Rc::new(RefCell::new(Vec::new()));
2771        let received_clone = received.clone();
2772        let handler = ShareableMessageHandler::from_typed(move |d: &CustomData| {
2773            received_clone.borrow_mut().push(d.clone());
2774        });
2775        msgbus::subscribe_any(topic.into(), handler, None);
2776
2777        actor.py_publish_data(&data.data_type, &data);
2778
2779        let received = received.borrow();
2780        assert_eq!(received.len(), 1);
2781        assert_eq!(received[0].data_type, data.data_type);
2782    }
2783
2784    #[rstest]
2785    fn test_publish_signal_delivers_to_customdata_subscriber(
2786        clock: Rc<RefCell<TestClock>>,
2787        cache: Rc<RefCell<Cache>>,
2788        trader_id: TraderId,
2789    ) {
2790        use crate::{
2791            msgbus::{
2792                self, MessageBus, Pattern, get_message_bus, typed_handler::ShareableMessageHandler,
2793            },
2794            signal::Signal,
2795        };
2796
2797        *get_message_bus().borrow_mut() = MessageBus::default();
2798
2799        let actor = create_registered_actor(clock, cache, trader_id);
2800
2801        // Signals travel as `CustomData` on the bus so persistence and other
2802        // `CustomData`-aware subscribers pick them up. Downcast inside the handler.
2803        let received: Rc<RefCell<Vec<Signal>>> = Rc::new(RefCell::new(Vec::new()));
2804        let received_clone = received.clone();
2805        let handler = ShareableMessageHandler::from_typed(move |data: &CustomData| {
2806            if let Some(sig) = data.data.as_any().downcast_ref::<Signal>() {
2807                received_clone.borrow_mut().push(sig.clone());
2808            }
2809        });
2810        let pattern: crate::msgbus::MStr<Pattern> = "data.Signal*".to_string().into();
2811        msgbus::subscribe_any(pattern, handler, None);
2812
2813        pyo3::Python::initialize();
2814
2815        Python::attach(|py| {
2816            let val1: Py<PyAny> = 1.0_f64.into_py_any_unwrap(py);
2817            let val2: Py<PyAny> = "HIGH".into_py_any_unwrap(py);
2818            actor.py_publish_signal(py, "example", val1, 0).unwrap();
2819            actor
2820                .py_publish_signal(py, "risk", val2, 1_700_000_000_000_000_000)
2821                .unwrap();
2822        });
2823
2824        let received = received.borrow();
2825        assert_eq!(received.len(), 2);
2826        assert_eq!(received[0].name.as_str(), "example");
2827        assert_eq!(received[0].value, "1.0");
2828        assert_eq!(received[1].name.as_str(), "risk");
2829        assert_eq!(received[1].value, "HIGH");
2830        assert_eq!(
2831            received[1].ts_event,
2832            UnixNanos::from(1_700_000_000_000_000_000_u64)
2833        );
2834    }
2835
2836    #[rstest]
2837    fn test_publish_signal_accepts_numeric_py_values(
2838        clock: Rc<RefCell<TestClock>>,
2839        cache: Rc<RefCell<Cache>>,
2840        trader_id: TraderId,
2841    ) {
2842        use crate::{
2843            msgbus::{
2844                self, MessageBus, Pattern, get_message_bus, typed_handler::ShareableMessageHandler,
2845            },
2846            signal::Signal,
2847        };
2848
2849        *get_message_bus().borrow_mut() = MessageBus::default();
2850
2851        let actor = create_registered_actor(clock, cache, trader_id);
2852
2853        let received: Rc<RefCell<Vec<Signal>>> = Rc::new(RefCell::new(Vec::new()));
2854        let received_clone = received.clone();
2855        let handler = ShareableMessageHandler::from_typed(move |data: &CustomData| {
2856            if let Some(sig) = data.data.as_any().downcast_ref::<Signal>() {
2857                received_clone.borrow_mut().push(sig.clone());
2858            }
2859        });
2860        let pattern: crate::msgbus::MStr<Pattern> = "data.Signal*".to_string().into();
2861        msgbus::subscribe_any(pattern, handler, None);
2862
2863        pyo3::Python::initialize();
2864
2865        Python::attach(|py| {
2866            let int_value: Py<PyAny> = 42_i64.into_py_any_unwrap(py);
2867            let float_value: Py<PyAny> = 3.5_f64.into_py_any_unwrap(py);
2868            let bool_value: Py<PyAny> = true.into_py_any_unwrap(py);
2869            actor.py_publish_signal(py, "count", int_value, 0).unwrap();
2870            actor
2871                .py_publish_signal(py, "ratio", float_value, 0)
2872                .unwrap();
2873            actor
2874                .py_publish_signal(py, "active", bool_value, 0)
2875                .unwrap();
2876        });
2877
2878        let received = received.borrow();
2879        assert_eq!(received.len(), 3);
2880        assert_eq!(received[0].value, "42");
2881        assert_eq!(received[1].value, "3.5");
2882        assert_eq!(received[2].value, "True");
2883    }
2884
2885    #[rstest]
2886    fn test_subscribe_and_unsubscribe_signal_compile(
2887        clock: Rc<RefCell<TestClock>>,
2888        cache: Rc<RefCell<Cache>>,
2889        trader_id: TraderId,
2890    ) {
2891        use crate::msgbus::{MessageBus, get_message_bus};
2892
2893        *get_message_bus().borrow_mut() = MessageBus::default();
2894
2895        let mut actor = create_registered_actor(clock, cache, trader_id);
2896        actor.py_subscribe_signal("example", None);
2897        actor.py_unsubscribe_signal("example");
2898        actor.py_subscribe_signal("", None);
2899        actor.py_unsubscribe_signal("");
2900    }
2901
2902    #[rstest]
2903    fn test_py_subscribe_signal_forwards_priority(
2904        clock: Rc<RefCell<TestClock>>,
2905        cache: Rc<RefCell<Cache>>,
2906        trader_id: TraderId,
2907    ) {
2908        use crate::msgbus::{MessageBus, get_message_bus, switchboard::get_signal_topic};
2909
2910        *get_message_bus().borrow_mut() = MessageBus::default();
2911
2912        let mut actor = create_registered_actor(clock, cache, trader_id);
2913        actor.py_subscribe_signal("trigger", Some(50));
2914
2915        // The PyO3 binding must forward the priority to the bus unchanged.
2916        let topic = get_signal_topic("trigger");
2917        let subs = get_message_bus().borrow_mut().matching_subscriptions(topic);
2918        assert_eq!(subs.len(), 1);
2919        assert_eq!(subs[0].priority, 50);
2920    }
2921
2922    #[rstest]
2923    fn test_publish_data_dispatches_to_python_on_data(
2924        clock: Rc<RefCell<TestClock>>,
2925        cache: Rc<RefCell<Cache>>,
2926        trader_id: TraderId,
2927    ) {
2928        use crate::msgbus::{MessageBus, get_message_bus};
2929
2930        *get_message_bus().borrow_mut() = MessageBus::default();
2931
2932        pyo3::Python::initialize();
2933
2934        Python::attach(|py| {
2935            let py_actor = create_tracking_python_actor(py).unwrap();
2936
2937            let mut rust_actor = PyDataActor::new(None);
2938            rust_actor.set_python_instance(py_actor.clone_ref(py));
2939            rust_actor.register(trader_id, clock, cache).unwrap();
2940            rust_actor.register_in_global_registries();
2941            rust_actor.py_start().unwrap();
2942
2943            let data = stub_custom_data(1, 42, None, None);
2944            rust_actor
2945                .py_subscribe_data(py, data.data_type.clone(), None, None)
2946                .unwrap();
2947
2948            rust_actor.py_publish_data(&data.data_type, &data);
2949            rust_actor.py_publish_data(&data.data_type, &data);
2950
2951            assert!(python_method_was_called(&py_actor, py, "on_data"));
2952            assert_eq!(python_method_call_count(&py_actor, py, "on_data"), 2);
2953        });
2954    }
2955
2956    #[rstest]
2957    fn test_publish_signal_dispatches_to_python_on_signal(
2958        clock: Rc<RefCell<TestClock>>,
2959        cache: Rc<RefCell<Cache>>,
2960        trader_id: TraderId,
2961    ) {
2962        use crate::msgbus::{MessageBus, get_message_bus};
2963
2964        *get_message_bus().borrow_mut() = MessageBus::default();
2965
2966        pyo3::Python::initialize();
2967
2968        Python::attach(|py| {
2969            let py_actor = create_tracking_python_actor(py).unwrap();
2970
2971            let mut rust_actor = PyDataActor::new(None);
2972            rust_actor.set_python_instance(py_actor.clone_ref(py));
2973            rust_actor.register(trader_id, clock, cache).unwrap();
2974            rust_actor.register_in_global_registries();
2975            rust_actor.py_start().unwrap();
2976
2977            rust_actor.py_subscribe_signal("example", None);
2978            let val1: Py<PyAny> = "1.5".into_py_any_unwrap(py);
2979            let val2: Py<PyAny> = 2.0_f64.into_py_any_unwrap(py);
2980            rust_actor
2981                .py_publish_signal(py, "example", val1, 0)
2982                .unwrap();
2983            rust_actor
2984                .py_publish_signal(py, "example", val2, 1_700_000_000_000_000_000)
2985                .unwrap();
2986
2987            assert!(python_method_was_called(&py_actor, py, "on_signal"));
2988            assert_eq!(python_method_call_count(&py_actor, py, "on_signal"), 2);
2989        });
2990    }
2991
2992    #[rstest]
2993    fn test_unsubscribe_signal_stops_python_dispatch(
2994        clock: Rc<RefCell<TestClock>>,
2995        cache: Rc<RefCell<Cache>>,
2996        trader_id: TraderId,
2997    ) {
2998        use crate::msgbus::{MessageBus, get_message_bus};
2999
3000        *get_message_bus().borrow_mut() = MessageBus::default();
3001
3002        pyo3::Python::initialize();
3003
3004        Python::attach(|py| {
3005            let py_actor = create_tracking_python_actor(py).unwrap();
3006
3007            let mut rust_actor = PyDataActor::new(None);
3008            rust_actor.set_python_instance(py_actor.clone_ref(py));
3009            rust_actor.register(trader_id, clock, cache).unwrap();
3010            rust_actor.register_in_global_registries();
3011            rust_actor.py_start().unwrap();
3012
3013            rust_actor.py_subscribe_signal("example", None);
3014            let val1: Py<PyAny> = "1".into_py_any_unwrap(py);
3015            let val2: Py<PyAny> = "2".into_py_any_unwrap(py);
3016            rust_actor
3017                .py_publish_signal(py, "example", val1, 0)
3018                .unwrap();
3019
3020            rust_actor.py_unsubscribe_signal("example");
3021            rust_actor
3022                .py_publish_signal(py, "example", val2, 0)
3023                .unwrap();
3024
3025            assert_eq!(python_method_call_count(&py_actor, py, "on_signal"), 1);
3026        });
3027    }
3028
3029    #[rstest]
3030    fn test_subscribe_signal_wildcard_dispatches_all_names_to_python(
3031        clock: Rc<RefCell<TestClock>>,
3032        cache: Rc<RefCell<Cache>>,
3033        trader_id: TraderId,
3034    ) {
3035        use crate::msgbus::{MessageBus, get_message_bus};
3036
3037        *get_message_bus().borrow_mut() = MessageBus::default();
3038
3039        pyo3::Python::initialize();
3040
3041        Python::attach(|py| {
3042            let py_actor = create_tracking_python_actor(py).unwrap();
3043
3044            let mut rust_actor = PyDataActor::new(None);
3045            rust_actor.set_python_instance(py_actor.clone_ref(py));
3046            rust_actor.register(trader_id, clock, cache).unwrap();
3047            rust_actor.register_in_global_registries();
3048            rust_actor.py_start().unwrap();
3049
3050            rust_actor.py_subscribe_signal("", None);
3051            let val1: Py<PyAny> = "1".into_py_any_unwrap(py);
3052            let val2: Py<PyAny> = "2".into_py_any_unwrap(py);
3053            let val3: Py<PyAny> = "3".into_py_any_unwrap(py);
3054            rust_actor.py_publish_signal(py, "alpha", val1, 0).unwrap();
3055            rust_actor.py_publish_signal(py, "beta", val2, 0).unwrap();
3056            rust_actor.py_publish_signal(py, "gamma", val3, 0).unwrap();
3057
3058            assert_eq!(python_method_call_count(&py_actor, py, "on_signal"), 3);
3059        });
3060    }
3061
3062    #[rstest]
3063    fn test_signal_customdata_unwraps_to_python_signal(
3064        clock: Rc<RefCell<TestClock>>,
3065        cache: Rc<RefCell<Cache>>,
3066        trader_id: TraderId,
3067    ) {
3068        // Exercises the `Signal::to_pyobject` path: a `CustomData` wrapping a
3069        // `Signal` reaches Python `on_data`, and the PyO3 `.data` getter must
3070        // successfully unwrap the inner `Arc<dyn CustomDataTrait>` into a
3071        // Python `Signal`. Without `Signal::to_pyobject`, the getter raises
3072        // `TypeError` and this assertion fails.
3073        use crate::msgbus::{MessageBus, get_message_bus};
3074
3075        *get_message_bus().borrow_mut() = MessageBus::default();
3076
3077        pyo3::Python::initialize();
3078
3079        Python::attach(|py| {
3080            let capture_code = c_str!(
3081                r#"
3082class CapturingActor:
3083    def __init__(self):
3084        self.captured = []
3085
3086    def on_start(self): pass
3087    def on_stop(self): pass
3088    def on_resume(self): pass
3089    def on_reset(self): pass
3090    def on_dispose(self): pass
3091    def on_degrade(self): pass
3092    def on_fault(self): pass
3093    def on_signal(self, signal): pass
3094
3095    def on_data(self, custom):
3096        # Exercise the CustomData.data getter: raises TypeError if the
3097        # inner payload cannot be converted back to a Python object.
3098        inner = custom.data
3099        self.captured.append((type(inner).__name__, inner.name, inner.value))
3100"#
3101            );
3102            py.run(capture_code, None, None).unwrap();
3103            let cls = py.eval(c_str!("CapturingActor"), None, None).unwrap();
3104            let py_actor: Py<PyAny> = cls.call0().unwrap().unbind();
3105
3106            let mut rust_actor = PyDataActor::new(None);
3107            rust_actor.set_python_instance(py_actor.clone_ref(py));
3108            rust_actor.register(trader_id, clock, cache).unwrap();
3109            rust_actor.register_in_global_registries();
3110            rust_actor.py_start().unwrap();
3111
3112            // Subscribe as custom-data for the signal's advertised DataType
3113            // (`data.SignalExample`) so `on_data` fires with the wrapping CustomData.
3114            let data_type = DataType::new("SignalExample", None, None);
3115            rust_actor
3116                .py_subscribe_data(py, data_type, None, None)
3117                .unwrap();
3118
3119            let val: Py<PyAny> = "1.5".into_py_any_unwrap(py);
3120            rust_actor.py_publish_signal(py, "example", val, 0).unwrap();
3121
3122            let captured = py_actor
3123                .bind(py)
3124                .getattr("captured")
3125                .unwrap()
3126                .extract::<Vec<(String, String, String)>>()
3127                .unwrap();
3128            assert_eq!(captured.len(), 1);
3129            assert_eq!(captured[0].0, "Signal");
3130            assert_eq!(captured[0].1, "example");
3131            assert_eq!(captured[0].2, "1.5");
3132        });
3133    }
3134
3135    #[rstest]
3136    fn test_add_and_update_synthetic_via_pyo3(
3137        clock: Rc<RefCell<TestClock>>,
3138        cache: Rc<RefCell<Cache>>,
3139        trader_id: TraderId,
3140    ) {
3141        use nautilus_model::{
3142            identifiers::{InstrumentId, Symbol},
3143            instruments::SyntheticInstrument,
3144        };
3145
3146        let actor = create_registered_actor(clock, cache.clone(), trader_id);
3147
3148        let comp1 = InstrumentId::from_str("BTC-USD.VENUE").unwrap();
3149        let comp2 = InstrumentId::from_str("ETH-USD.VENUE").unwrap();
3150        let formula = format!("({comp1} + {comp2}) / 2.0");
3151        let synthetic = SyntheticInstrument::new(
3152            Symbol::from("SYN"),
3153            2,
3154            vec![comp1, comp2],
3155            &formula,
3156            UnixNanos::default(),
3157            UnixNanos::default(),
3158        );
3159        let synthetic_id = synthetic.id;
3160
3161        actor.py_add_synthetic(synthetic.clone()).unwrap();
3162        assert!(cache.borrow().synthetic(&synthetic_id).is_some());
3163
3164        // Adding again raises
3165        assert!(actor.py_add_synthetic(synthetic).is_err());
3166
3167        let new_formula = format!("{comp1} + {comp2}");
3168        let updated = SyntheticInstrument::new(
3169            Symbol::from("SYN"),
3170            2,
3171            vec![comp1, comp2],
3172            &new_formula,
3173            UnixNanos::default(),
3174            UnixNanos::default(),
3175        );
3176        actor.py_update_synthetic(updated).unwrap();
3177        assert_eq!(
3178            cache.borrow().synthetic(&synthetic_id).unwrap().formula,
3179            new_formula
3180        );
3181
3182        // Updating a non-existent raises
3183        let missing = SyntheticInstrument::new(
3184            Symbol::from("GONE"),
3185            2,
3186            vec![comp1, comp2],
3187            &formula,
3188            UnixNanos::default(),
3189            UnixNanos::default(),
3190        );
3191        assert!(actor.py_update_synthetic(missing).is_err());
3192    }
3193
3194    #[rstest]
3195    fn test_book_at_interval_invalid_interval_ms(
3196        clock: Rc<RefCell<TestClock>>,
3197        cache: Rc<RefCell<Cache>>,
3198        trader_id: TraderId,
3199        audusd_sim: CurrencyPair,
3200    ) {
3201        pyo3::Python::initialize();
3202
3203        let mut actor = create_registered_actor(clock, cache, trader_id);
3204
3205        pyo3::Python::attach(|py| {
3206            let result = actor.py_subscribe_book_at_interval(
3207                py,
3208                audusd_sim.id,
3209                BookType::L2_MBP,
3210                0,
3211                None,
3212                None,
3213                None,
3214            );
3215            assert!(result.is_err());
3216            assert_eq!(
3217                result.unwrap_err().to_string(),
3218                "ValueError: interval_ms must be > 0"
3219            );
3220
3221            let result = actor.py_unsubscribe_book_at_interval(py, audusd_sim.id, 0, None, None);
3222            assert!(result.is_err());
3223            assert_eq!(
3224                result.unwrap_err().to_string(),
3225                "ValueError: interval_ms must be > 0"
3226            );
3227        });
3228    }
3229
3230    #[rstest]
3231    fn test_request_methods_signatures_exist() {
3232        let actor = create_unregistered_actor();
3233        assert!(actor.trader_id().is_none());
3234    }
3235
3236    #[rstest]
3237    fn test_data_actor_trait_implementation(
3238        clock: Rc<RefCell<TestClock>>,
3239        cache: Rc<RefCell<Cache>>,
3240        trader_id: TraderId,
3241    ) {
3242        let actor = create_registered_actor(clock, cache, trader_id);
3243        let state = actor.state();
3244        assert_eq!(state, ComponentState::Ready);
3245    }
3246
3247    fn sample_instrument() -> CurrencyPair {
3248        audusd_sim()
3249    }
3250
3251    fn sample_data() -> CustomData {
3252        stub_custom_data(1, 42, None, None)
3253    }
3254
3255    fn sample_time_event() -> TimeEvent {
3256        TimeEvent::new(
3257            Ustr::from("test_timer"),
3258            UUID4::new(),
3259            UnixNanos::default(),
3260            UnixNanos::default(),
3261        )
3262    }
3263
3264    fn sample_signal() -> Signal {
3265        Signal::new(
3266            Ustr::from("test_signal"),
3267            "1.0".to_string(),
3268            UnixNanos::default(),
3269            UnixNanos::default(),
3270        )
3271    }
3272
3273    fn sample_quote() -> QuoteTick {
3274        let instrument = sample_instrument();
3275        QuoteTick::new(
3276            instrument.id,
3277            Price::from("1.00000"),
3278            Price::from("1.00001"),
3279            Quantity::from(100_000),
3280            Quantity::from(100_000),
3281            UnixNanos::default(),
3282            UnixNanos::default(),
3283        )
3284    }
3285
3286    fn sample_trade() -> TradeTick {
3287        let instrument = sample_instrument();
3288        TradeTick::new(
3289            instrument.id,
3290            Price::from("1.00000"),
3291            Quantity::from(100_000),
3292            AggressorSide::Buyer,
3293            TradeId::new("123456"),
3294            UnixNanos::default(),
3295            UnixNanos::default(),
3296        )
3297    }
3298
3299    fn sample_bar() -> Bar {
3300        let instrument = sample_instrument();
3301        let bar_type =
3302            BarType::from_str(&format!("{}-1-MINUTE-LAST-INTERNAL", instrument.id)).unwrap();
3303        Bar::new(
3304            bar_type,
3305            Price::from("1.00000"),
3306            Price::from("1.00010"),
3307            Price::from("0.99990"),
3308            Price::from("1.00005"),
3309            Quantity::from(100_000),
3310            UnixNanos::default(),
3311            UnixNanos::default(),
3312        )
3313    }
3314
3315    fn sample_book() -> OrderBook {
3316        OrderBook::new(sample_instrument().id, BookType::L2_MBP)
3317    }
3318
3319    fn sample_book_deltas() -> OrderBookDeltas {
3320        let instrument = sample_instrument();
3321        let delta =
3322            OrderBookDelta::clear(instrument.id, 0, UnixNanos::default(), UnixNanos::default());
3323        OrderBookDeltas::new(instrument.id, vec![delta])
3324    }
3325
3326    fn sample_mark_price() -> MarkPriceUpdate {
3327        MarkPriceUpdate::new(
3328            sample_instrument().id,
3329            Price::from("1.00000"),
3330            UnixNanos::default(),
3331            UnixNanos::default(),
3332        )
3333    }
3334
3335    fn sample_index_price() -> IndexPriceUpdate {
3336        IndexPriceUpdate::new(
3337            sample_instrument().id,
3338            Price::from("1.00000"),
3339            UnixNanos::default(),
3340            UnixNanos::default(),
3341        )
3342    }
3343
3344    fn sample_funding_rate() -> FundingRateUpdate {
3345        FundingRateUpdate::new(
3346            sample_instrument().id,
3347            "0.0001".parse().unwrap(),
3348            None,
3349            None,
3350            UnixNanos::default(),
3351            UnixNanos::default(),
3352        )
3353    }
3354
3355    fn sample_instrument_status() -> InstrumentStatus {
3356        InstrumentStatus::new(
3357            sample_instrument().id,
3358            MarketStatusAction::Trading,
3359            UnixNanos::default(),
3360            UnixNanos::default(),
3361            None,
3362            None,
3363            None,
3364            None,
3365            None,
3366        )
3367    }
3368
3369    fn sample_instrument_close() -> InstrumentClose {
3370        InstrumentClose::new(
3371            sample_instrument().id,
3372            Price::from("1.00000"),
3373            InstrumentCloseType::EndOfSession,
3374            UnixNanos::default(),
3375            UnixNanos::default(),
3376        )
3377    }
3378
3379    fn sample_option_greeks() -> OptionGreeks {
3380        OptionGreeks {
3381            instrument_id: sample_instrument().id,
3382            convention: GreeksConvention::BlackScholes,
3383            greeks: OptionGreekValues {
3384                delta: 0.55,
3385                gamma: 0.03,
3386                vega: 0.12,
3387                theta: -0.05,
3388                rho: 0.01,
3389            },
3390            mark_iv: Some(0.25),
3391            bid_iv: None,
3392            ask_iv: None,
3393            underlying_price: None,
3394            open_interest: None,
3395            ts_event: UnixNanos::default(),
3396            ts_init: UnixNanos::default(),
3397        }
3398    }
3399
3400    fn sample_option_chain() -> OptionChainSlice {
3401        OptionChainSlice {
3402            series_id: OptionSeriesId::new(
3403                Venue::from("SIM"),
3404                Ustr::from("AUD"),
3405                Ustr::from("USD"),
3406                UnixNanos::from(1_711_036_800_000_000_000),
3407            ),
3408            atm_strike: None,
3409            calls: Default::default(),
3410            puts: Default::default(),
3411            ts_event: UnixNanos::default(),
3412            ts_init: UnixNanos::default(),
3413        }
3414    }
3415
3416    #[cfg(feature = "defi")]
3417    fn sample_block() -> Block {
3418        Block::new(
3419            "0x1234567890abcdef".to_string(),
3420            "0xabcdef1234567890".to_string(),
3421            12345,
3422            "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0".into(),
3423            21000,
3424            20000,
3425            UnixNanos::default(),
3426            Some(Blockchain::Ethereum),
3427        )
3428    }
3429
3430    #[cfg(feature = "defi")]
3431    fn sample_pool_components() -> (Arc<Chain>, Arc<Dex>, Pool) {
3432        let chain = Arc::new(Chain::new(Blockchain::Ethereum, 1));
3433        let dex = Arc::new(Dex::new(
3434            Chain::new(Blockchain::Ethereum, 1),
3435            DexType::UniswapV3,
3436            "0x1F98431c8aD98523631AE4a59f267346ea31F984",
3437            0,
3438            AmmType::CLAMM,
3439            "PoolCreated",
3440            "Swap",
3441            "Mint",
3442            "Burn",
3443            "Collect",
3444        ));
3445        let token0 = Token::new(
3446            chain.clone(),
3447            "0xa0b86a33e6441c8c06dd7b111a8c4e82e2b2a5e1"
3448                .parse()
3449                .unwrap(),
3450            "USDC".into(),
3451            "USD Coin".into(),
3452            6,
3453        );
3454        let token1 = Token::new(
3455            chain.clone(),
3456            "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
3457                .parse()
3458                .unwrap(),
3459            "WETH".into(),
3460            "Wrapped Ether".into(),
3461            18,
3462        );
3463        let pool_address = "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8"
3464            .parse()
3465            .unwrap();
3466        let pool_identifier: PoolIdentifier = "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8"
3467            .parse()
3468            .unwrap();
3469
3470        let pool = Pool::new(
3471            chain.clone(),
3472            dex.clone(),
3473            pool_address,
3474            pool_identifier,
3475            12345,
3476            token0,
3477            token1,
3478            Some(500),
3479            Some(10),
3480            UnixNanos::default(),
3481        );
3482
3483        (chain, dex, pool)
3484    }
3485
3486    #[cfg(feature = "defi")]
3487    fn sample_pool_swap() -> PoolSwap {
3488        let (chain, dex, pool) = sample_pool_components();
3489        PoolSwap::new(
3490            chain,
3491            dex,
3492            pool.instrument_id,
3493            pool.pool_identifier,
3494            12345,
3495            "0xabc123".to_string(),
3496            0,
3497            0,
3498            UnixNanos::default(),
3499            UnixNanos::default(),
3500            "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
3501                .parse()
3502                .unwrap(),
3503            "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
3504                .parse()
3505                .unwrap(),
3506            I256::from_str("1000000000000000000").unwrap(),
3507            I256::from_str("400000000000000").unwrap(),
3508            U160::from(59000000000000u128),
3509            1000000,
3510            100,
3511        )
3512    }
3513
3514    #[cfg(feature = "defi")]
3515    fn sample_pool_liquidity_update() -> PoolLiquidityUpdate {
3516        let (chain, dex, pool) = sample_pool_components();
3517        PoolLiquidityUpdate::new(
3518            chain,
3519            dex,
3520            pool.instrument_id,
3521            pool.pool_identifier,
3522            PoolLiquidityUpdateType::Mint,
3523            12345,
3524            "0xabc123".to_string(),
3525            0,
3526            0,
3527            Some(
3528                "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
3529                    .parse()
3530                    .unwrap(),
3531            ),
3532            "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
3533                .parse()
3534                .unwrap(),
3535            1000,
3536            U256::from(1_000u64),
3537            U256::from(2_000u64),
3538            -10,
3539            10,
3540            UnixNanos::default(),
3541            UnixNanos::default(),
3542        )
3543    }
3544
3545    #[cfg(feature = "defi")]
3546    fn sample_pool_fee_collect() -> PoolFeeCollect {
3547        let (chain, dex, pool) = sample_pool_components();
3548        PoolFeeCollect::new(
3549            chain,
3550            dex,
3551            pool.instrument_id,
3552            pool.pool_identifier,
3553            12345,
3554            "0xabc123".to_string(),
3555            0,
3556            0,
3557            "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
3558                .parse()
3559                .unwrap(),
3560            100,
3561            200,
3562            -10,
3563            10,
3564            UnixNanos::default(),
3565            UnixNanos::default(),
3566        )
3567    }
3568
3569    #[cfg(feature = "defi")]
3570    fn sample_pool_flash() -> PoolFlash {
3571        let (chain, dex, pool) = sample_pool_components();
3572        PoolFlash::new(
3573            chain,
3574            dex,
3575            pool.instrument_id,
3576            pool.pool_identifier,
3577            12345,
3578            "0xabc123".to_string(),
3579            0,
3580            0,
3581            UnixNanos::default(),
3582            UnixNanos::default(),
3583            "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
3584                .parse()
3585                .unwrap(),
3586            "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
3587                .parse()
3588                .unwrap(),
3589            U256::from(100u64),
3590            U256::from(200u64),
3591            U256::from(101u64),
3592            U256::from(201u64),
3593        )
3594    }
3595
3596    const TRACKING_ACTOR_CODE: &std::ffi::CStr = c_str!(
3597        r#"
3598class TrackingActor:
3599    """A mock Python actor that tracks all method calls."""
3600
3601    TRACKED_METHODS = {
3602        "on_start",
3603        "on_stop",
3604        "on_resume",
3605        "on_reset",
3606        "on_dispose",
3607        "on_degrade",
3608        "on_fault",
3609        "on_save",
3610        "on_load",
3611        "on_time_event",
3612        "on_data",
3613        "on_signal",
3614        "on_instrument",
3615        "on_quote",
3616        "on_trade",
3617        "on_bar",
3618        "on_book",
3619        "on_book_deltas",
3620        "on_mark_price",
3621        "on_index_price",
3622        "on_funding_rate",
3623        "on_instrument_status",
3624        "on_instrument_close",
3625        "on_option_greeks",
3626        "on_option_chain",
3627        "on_historical_data",
3628        "on_historical_quotes",
3629        "on_historical_trades",
3630        "on_historical_funding_rates",
3631        "on_historical_bars",
3632        "on_historical_mark_prices",
3633        "on_historical_index_prices",
3634        "on_block",
3635        "on_pool",
3636        "on_pool_swap",
3637        "on_pool_liquidity_update",
3638        "on_pool_fee_collect",
3639        "on_pool_flash",
3640    }
3641
3642    def __init__(self):
3643        self.calls = []
3644
3645    def _record(self, method_name, *args):
3646        self.calls.append((method_name, args))
3647
3648    def was_called(self, method_name):
3649        return any(call[0] == method_name for call in self.calls)
3650
3651    def call_count(self, method_name):
3652        return sum(1 for call in self.calls if call[0] == method_name)
3653
3654    def last_loaded_state(self):
3655
3656        for method_name, args in reversed(self.calls):
3657            if method_name == "on_load":
3658                return args[0]
3659        return None
3660
3661    def on_save(self):
3662        self._record("on_save")
3663        return {"actor": b"saved"}
3664
3665    def on_load(self, state):
3666        self._record("on_load", dict(state))
3667
3668    def __getattr__(self, name):
3669        if name in self.TRACKED_METHODS:
3670            return lambda *args: self._record(name, *args)
3671        raise AttributeError(name)
3672"#
3673    );
3674
3675    fn create_tracking_python_actor(py: Python<'_>) -> PyResult<Py<PyAny>> {
3676        py.run(TRACKING_ACTOR_CODE, None, None)?;
3677        let tracking_actor_class = py.eval(c_str!("TrackingActor"), None, None)?;
3678        let instance = tracking_actor_class.call0()?;
3679        Ok(instance.unbind())
3680    }
3681
3682    fn python_method_was_called(py_actor: &Py<PyAny>, py: Python<'_>, method_name: &str) -> bool {
3683        py_actor
3684            .call_method1(py, "was_called", (method_name,))
3685            .and_then(|r| r.extract::<bool>(py))
3686            .unwrap_or(false)
3687    }
3688
3689    fn python_method_call_count(py_actor: &Py<PyAny>, py: Python<'_>, method_name: &str) -> i32 {
3690        py_actor
3691            .call_method1(py, "call_count", (method_name,))
3692            .and_then(|r| r.extract::<i32>(py))
3693            .unwrap_or(0)
3694    }
3695
3696    fn python_last_loaded_state(
3697        py_actor: &Py<PyAny>,
3698        py: Python<'_>,
3699    ) -> Option<HashMap<String, Vec<u8>>> {
3700        py_actor
3701            .call_method0(py, "last_loaded_state")
3702            .and_then(|result| result.extract::<Option<HashMap<String, Vec<u8>>>>(py))
3703            .unwrap_or(None)
3704    }
3705
3706    const TRACKING_INDICATOR_CODE: &std::ffi::CStr = c_str!(
3707        r#"
3708class TrackingIndicator:
3709    def __init__(self, events=None):
3710        self.initialized = False
3711        self.calls = []
3712        self.events = events
3713
3714    def handle_quote_tick(self, quote):
3715        self.calls.append("quote")
3716        if self.events is not None:
3717            self.events.append("indicator:quote")
3718
3719    def handle_trade_tick(self, trade):
3720        self.calls.append("trade")
3721        if self.events is not None:
3722            self.events.append("indicator:trade")
3723
3724    def handle_bar(self, bar):
3725        self.calls.append("bar")
3726        if self.events is not None:
3727            self.events.append("indicator:bar")
3728
3729    def call_count(self, name):
3730        return self.calls.count(name)
3731
3732class RaisingIndicator:
3733    initialized = True
3734
3735    def handle_quote_tick(self, quote):
3736        raise RuntimeError("indicator failed")
3737
3738class IndicatorEventActor:
3739    def __init__(self, events):
3740        self.events = events
3741
3742    def on_start(self):
3743        pass
3744
3745    def on_quote(self, quote):
3746        self.events.append("actor:quote")
3747
3748    def on_trade(self, trade):
3749        self.events.append("actor:trade")
3750
3751    def on_bar(self, bar):
3752        self.events.append("actor:bar")
3753"#
3754    );
3755
3756    fn create_tracking_python_indicator(py: Python<'_>) -> PyResult<Py<PyAny>> {
3757        py.run(TRACKING_INDICATOR_CODE, None, None)?;
3758        let indicator_class = py.eval(c_str!("TrackingIndicator"), None, None)?;
3759        Ok(indicator_class.call0()?.unbind())
3760    }
3761
3762    fn create_event_tracking_python_indicator(
3763        py: Python<'_>,
3764        events: &Bound<'_, PyList>,
3765    ) -> PyResult<Py<PyAny>> {
3766        py.run(TRACKING_INDICATOR_CODE, None, None)?;
3767        let indicator_class = py.eval(c_str!("TrackingIndicator"), None, None)?;
3768        Ok(indicator_class.call1((events,))?.unbind())
3769    }
3770
3771    fn create_raising_python_indicator(py: Python<'_>) -> PyResult<Py<PyAny>> {
3772        py.run(TRACKING_INDICATOR_CODE, None, None)?;
3773        let indicator_class = py.eval(c_str!("RaisingIndicator"), None, None)?;
3774        Ok(indicator_class.call0()?.unbind())
3775    }
3776
3777    fn create_indicator_event_actor(
3778        py: Python<'_>,
3779        events: &Bound<'_, PyList>,
3780    ) -> PyResult<Py<PyAny>> {
3781        py.run(TRACKING_INDICATOR_CODE, None, None)?;
3782        let actor_class = py.eval(c_str!("IndicatorEventActor"), None, None)?;
3783        Ok(actor_class.call1((events,))?.unbind())
3784    }
3785
3786    fn python_indicator_call_count(
3787        indicator: &Py<PyAny>,
3788        py: Python<'_>,
3789        method_name: &str,
3790    ) -> i32 {
3791        indicator
3792            .call_method1(py, "call_count", (method_name,))
3793            .and_then(|r| r.extract::<i32>(py))
3794            .unwrap_or(0)
3795    }
3796
3797    fn assert_python_dispatch<F>(
3798        py: Python<'_>,
3799        clock: Rc<RefCell<TestClock>>,
3800        cache: Rc<RefCell<Cache>>,
3801        trader_id: TraderId,
3802        method_name: &str,
3803        invoke: F,
3804    ) where
3805        F: FnOnce(&mut PyDataActor) -> anyhow::Result<()>,
3806    {
3807        let py_actor = create_tracking_python_actor(py).unwrap();
3808
3809        let mut rust_actor = PyDataActor::new(None);
3810        rust_actor.set_python_instance(py_actor.clone_ref(py));
3811        rust_actor.register(trader_id, clock, cache).unwrap();
3812
3813        let result = invoke(&mut rust_actor);
3814
3815        assert!(result.is_ok());
3816        assert!(python_method_was_called(&py_actor, py, method_name));
3817        assert_eq!(python_method_call_count(&py_actor, py, method_name), 1);
3818    }
3819
3820    #[rstest]
3821    #[case("on_start")]
3822    #[case("on_stop")]
3823    #[case("on_resume")]
3824    #[case("on_reset")]
3825    #[case("on_dispose")]
3826    #[case("on_degrade")]
3827    #[case("on_fault")]
3828    fn test_python_dispatch_lifecycle_matrix(
3829        clock: Rc<RefCell<TestClock>>,
3830        cache: Rc<RefCell<Cache>>,
3831        trader_id: TraderId,
3832        #[case] method_name: &str,
3833    ) {
3834        pyo3::Python::initialize();
3835
3836        Python::attach(|py| {
3837            assert_python_dispatch(py, clock, cache, trader_id, method_name, |rust_actor| {
3838                match method_name {
3839                    "on_start" => DataActor::on_start(rust_actor.inner_mut()),
3840                    "on_stop" => DataActor::on_stop(rust_actor.inner_mut()),
3841                    "on_resume" => DataActor::on_resume(rust_actor.inner_mut()),
3842                    "on_reset" => DataActor::on_reset(rust_actor.inner_mut()),
3843                    "on_dispose" => DataActor::on_dispose(rust_actor.inner_mut()),
3844                    "on_degrade" => DataActor::on_degrade(rust_actor.inner_mut()),
3845                    "on_fault" => DataActor::on_fault(rust_actor.inner_mut()),
3846                    _ => unreachable!("unhandled lifecycle case: {method_name}"),
3847                }
3848            });
3849        });
3850    }
3851
3852    #[rstest]
3853    #[case("on_save")]
3854    #[case("on_load")]
3855    fn test_python_dispatch_persistence_matrix(
3856        clock: Rc<RefCell<TestClock>>,
3857        cache: Rc<RefCell<Cache>>,
3858        trader_id: TraderId,
3859        #[case] method_name: &str,
3860    ) {
3861        pyo3::Python::initialize();
3862
3863        Python::attach(|py| {
3864            assert_python_dispatch(py, clock, cache, trader_id, method_name, |rust_actor| {
3865                match method_name {
3866                    "on_save" => {
3867                        let state = DataActor::on_save(rust_actor.inner()).unwrap();
3868                        assert_eq!(
3869                            state.get("actor").map(Vec::as_slice),
3870                            Some(b"saved".as_slice())
3871                        );
3872                        Ok(())
3873                    }
3874                    "on_load" => {
3875                        let mut state = IndexMap::new();
3876                        state.insert("actor".to_string(), b"loaded".to_vec());
3877                        DataActor::on_load(rust_actor.inner_mut(), state)
3878                    }
3879                    _ => unreachable!("unhandled persistence case: {method_name}"),
3880                }
3881            });
3882        });
3883    }
3884
3885    #[rstest]
3886    fn test_python_persistence_methods_convert_state(
3887        clock: Rc<RefCell<TestClock>>,
3888        cache: Rc<RefCell<Cache>>,
3889        trader_id: TraderId,
3890    ) {
3891        pyo3::Python::initialize();
3892
3893        Python::attach(|py| {
3894            let py_actor = create_tracking_python_actor(py).unwrap();
3895
3896            let mut rust_actor = PyDataActor::new(None);
3897            rust_actor.set_python_instance(py_actor.clone_ref(py));
3898            rust_actor.register(trader_id, clock, cache).unwrap();
3899
3900            let saved = rust_actor.py_save(py).unwrap();
3901            let saved_state = saved
3902                .bind(py)
3903                .extract::<HashMap<String, Vec<u8>>>()
3904                .unwrap();
3905            assert_eq!(
3906                saved_state.get("actor").map(Vec::as_slice),
3907                Some(&b"saved"[..])
3908            );
3909
3910            let load_state = PyDict::new(py);
3911            load_state
3912                .set_item("actor", PyBytes::new(py, b"loaded-from-python"))
3913                .unwrap();
3914
3915            rust_actor.py_load(&load_state).unwrap();
3916
3917            let loaded_state = python_last_loaded_state(&py_actor, py).unwrap();
3918            assert_eq!(
3919                loaded_state.get("actor").map(Vec::as_slice),
3920                Some(&b"loaded-from-python"[..])
3921            );
3922        });
3923    }
3924
3925    #[rstest]
3926    fn test_indicator_registration_exposes_readiness_and_registered_view(
3927        audusd_sim: CurrencyPair,
3928        bar_type: BarType,
3929    ) {
3930        pyo3::Python::initialize();
3931
3932        Python::attach(|py| {
3933            let mut rust_actor = PyDataActor::new(None);
3934            let indicator = create_tracking_python_indicator(py).unwrap();
3935
3936            assert_eq!(
3937                rust_actor
3938                    .py_registered_indicators(py)
3939                    .unwrap()
3940                    .bind(py)
3941                    .len()
3942                    .unwrap(),
3943                0
3944            );
3945            assert!(!rust_actor.py_indicators_initialized(py).unwrap());
3946
3947            rust_actor.py_register_indicator_for_quote_ticks(
3948                py,
3949                audusd_sim.id,
3950                indicator.clone_ref(py),
3951            );
3952            rust_actor.py_register_indicator_for_trade_ticks(
3953                py,
3954                audusd_sim.id,
3955                indicator.clone_ref(py),
3956            );
3957            rust_actor.py_register_indicator_for_bars(py, bar_type, indicator.clone_ref(py));
3958
3959            let registered = rust_actor.py_registered_indicators(py).unwrap();
3960            let registered = registered.bind(py);
3961
3962            assert_eq!(registered.len().unwrap(), 1);
3963            assert_eq!(
3964                registered.get_item(0).unwrap().as_ptr(),
3965                indicator.bind(py).as_ptr()
3966            );
3967            assert!(!rust_actor.py_indicators_initialized(py).unwrap());
3968
3969            indicator.bind(py).setattr("initialized", true).unwrap();
3970
3971            assert!(rust_actor.py_indicators_initialized(py).unwrap());
3972        });
3973    }
3974
3975    #[rstest]
3976    fn test_registered_indicators_receive_quote_trade_and_bar_before_actor_callbacks(
3977        clock: Rc<RefCell<TestClock>>,
3978        cache: Rc<RefCell<Cache>>,
3979        trader_id: TraderId,
3980    ) {
3981        pyo3::Python::initialize();
3982
3983        Python::attach(|py| {
3984            let events = PyList::empty(py);
3985            let py_actor = create_indicator_event_actor(py, &events).unwrap();
3986            let indicator = create_event_tracking_python_indicator(py, &events).unwrap();
3987
3988            let mut rust_actor = PyDataActor::new(None);
3989            rust_actor.set_python_instance(py_actor.clone_ref(py));
3990            rust_actor.register(trader_id, clock, cache).unwrap();
3991            Component::start(rust_actor.inner_mut()).unwrap();
3992
3993            let quote = sample_quote();
3994            let trade = sample_trade();
3995            let bar = sample_bar();
3996            let external_bar_type = BarType::from_str(&format!(
3997                "{}-1-MINUTE-LAST-EXTERNAL",
3998                bar.bar_type.instrument_id()
3999            ))
4000            .unwrap();
4001
4002            rust_actor.py_register_indicator_for_quote_ticks(
4003                py,
4004                quote.instrument_id,
4005                indicator.clone_ref(py),
4006            );
4007            rust_actor.py_register_indicator_for_trade_ticks(
4008                py,
4009                trade.instrument_id,
4010                indicator.clone_ref(py),
4011            );
4012            rust_actor.py_register_indicator_for_bars(
4013                py,
4014                external_bar_type,
4015                indicator.clone_ref(py),
4016            );
4017
4018            DataActor::handle_quote(rust_actor.inner_mut(), &quote);
4019            DataActor::handle_trade(rust_actor.inner_mut(), &trade);
4020            DataActor::handle_bar(rust_actor.inner_mut(), &bar);
4021
4022            let events = events.extract::<Vec<String>>().unwrap();
4023
4024            assert_eq!(python_indicator_call_count(&indicator, py, "quote"), 1);
4025            assert_eq!(python_indicator_call_count(&indicator, py, "trade"), 1);
4026            assert_eq!(python_indicator_call_count(&indicator, py, "bar"), 1);
4027            assert_eq!(
4028                events,
4029                vec![
4030                    "indicator:quote",
4031                    "actor:quote",
4032                    "indicator:trade",
4033                    "actor:trade",
4034                    "indicator:bar",
4035                    "actor:bar",
4036                ]
4037            );
4038        });
4039    }
4040
4041    #[rstest]
4042    fn test_registered_indicators_receive_live_data_when_actor_not_running(
4043        clock: Rc<RefCell<TestClock>>,
4044        cache: Rc<RefCell<Cache>>,
4045        trader_id: TraderId,
4046    ) {
4047        pyo3::Python::initialize();
4048
4049        Python::attach(|py| {
4050            let events = PyList::empty(py);
4051            let py_actor = create_indicator_event_actor(py, &events).unwrap();
4052            let indicator = create_event_tracking_python_indicator(py, &events).unwrap();
4053
4054            let mut rust_actor = PyDataActor::new(None);
4055            rust_actor.set_python_instance(py_actor.clone_ref(py));
4056            rust_actor.register(trader_id, clock, cache).unwrap();
4057
4058            let quote = sample_quote();
4059
4060            rust_actor.py_register_indicator_for_quote_ticks(
4061                py,
4062                quote.instrument_id,
4063                indicator.clone_ref(py),
4064            );
4065
4066            DataActor::handle_quote(rust_actor.inner_mut(), &quote);
4067
4068            let events = events.extract::<Vec<String>>().unwrap();
4069
4070            assert_eq!(python_indicator_call_count(&indicator, py, "quote"), 1);
4071            assert_eq!(events, vec!["indicator:quote"]);
4072        });
4073    }
4074
4075    #[rstest]
4076    fn test_registered_indicators_receive_historical_quote_trade_and_bar_batches() {
4077        pyo3::Python::initialize();
4078
4079        Python::attach(|py| {
4080            let mut rust_actor = PyDataActor::new(None);
4081            let indicator = create_tracking_python_indicator(py).unwrap();
4082            let quote = sample_quote();
4083            let trade = sample_trade();
4084            let bar = sample_bar();
4085            let quotes = vec![quote];
4086            let trades = vec![trade];
4087            let bars = vec![bar];
4088
4089            rust_actor.py_register_indicator_for_quote_ticks(
4090                py,
4091                quote.instrument_id,
4092                indicator.clone_ref(py),
4093            );
4094            rust_actor.py_register_indicator_for_trade_ticks(
4095                py,
4096                trade.instrument_id,
4097                indicator.clone_ref(py),
4098            );
4099            rust_actor.py_register_indicator_for_bars(py, bar.bar_type, indicator.clone_ref(py));
4100
4101            let client_id = ClientId::new("TEST");
4102            let quotes_response = QuotesResponse::new(
4103                UUID4::new(),
4104                client_id,
4105                quote.instrument_id,
4106                quotes,
4107                None,
4108                None,
4109                UnixNanos::default(),
4110                None,
4111            );
4112            let trades_response = TradesResponse::new(
4113                UUID4::new(),
4114                client_id,
4115                trade.instrument_id,
4116                trades,
4117                None,
4118                None,
4119                UnixNanos::default(),
4120                None,
4121            );
4122            let bars_response = BarsResponse::new(
4123                UUID4::new(),
4124                client_id,
4125                bar.bar_type,
4126                bars,
4127                None,
4128                None,
4129                UnixNanos::default(),
4130                None,
4131            );
4132
4133            DataActor::handle_quotes_response(rust_actor.inner_mut(), &quotes_response);
4134            DataActor::handle_trades_response(rust_actor.inner_mut(), &trades_response);
4135            DataActor::handle_bars_response(rust_actor.inner_mut(), &bars_response);
4136
4137            assert_eq!(python_indicator_call_count(&indicator, py, "quote"), 1);
4138            assert_eq!(python_indicator_call_count(&indicator, py, "trade"), 1);
4139            assert_eq!(python_indicator_call_count(&indicator, py, "bar"), 1);
4140        });
4141    }
4142
4143    #[rstest]
4144    fn test_indicators_initialized_requires_all_registered_indicators(audusd_sim: CurrencyPair) {
4145        pyo3::Python::initialize();
4146
4147        Python::attach(|py| {
4148            let mut rust_actor = PyDataActor::new(None);
4149            let first = create_tracking_python_indicator(py).unwrap();
4150            let second = create_tracking_python_indicator(py).unwrap();
4151
4152            rust_actor.py_register_indicator_for_quote_ticks(
4153                py,
4154                audusd_sim.id,
4155                first.clone_ref(py),
4156            );
4157            rust_actor.py_register_indicator_for_quote_ticks(
4158                py,
4159                audusd_sim.id,
4160                second.clone_ref(py),
4161            );
4162
4163            first.bind(py).setattr("initialized", true).unwrap();
4164
4165            assert!(!rust_actor.py_indicators_initialized(py).unwrap());
4166
4167            second.bind(py).setattr("initialized", true).unwrap();
4168
4169            assert!(rust_actor.py_indicators_initialized(py).unwrap());
4170        });
4171    }
4172
4173    #[rstest]
4174    fn test_duplicate_indicator_registration_does_not_duplicate_callbacks(
4175        clock: Rc<RefCell<TestClock>>,
4176        cache: Rc<RefCell<Cache>>,
4177        trader_id: TraderId,
4178    ) {
4179        pyo3::Python::initialize();
4180
4181        Python::attach(|py| {
4182            let mut rust_actor = PyDataActor::new(None);
4183            let indicator = create_tracking_python_indicator(py).unwrap();
4184            let quote = sample_quote();
4185            let trade = sample_trade();
4186            let bar = sample_bar();
4187            rust_actor.register(trader_id, clock, cache).unwrap();
4188            Component::start(rust_actor.inner_mut()).unwrap();
4189
4190            rust_actor.py_register_indicator_for_quote_ticks(
4191                py,
4192                quote.instrument_id,
4193                indicator.clone_ref(py),
4194            );
4195            rust_actor.py_register_indicator_for_quote_ticks(
4196                py,
4197                quote.instrument_id,
4198                indicator.clone_ref(py),
4199            );
4200            rust_actor.py_register_indicator_for_trade_ticks(
4201                py,
4202                trade.instrument_id,
4203                indicator.clone_ref(py),
4204            );
4205            rust_actor.py_register_indicator_for_trade_ticks(
4206                py,
4207                trade.instrument_id,
4208                indicator.clone_ref(py),
4209            );
4210            rust_actor.py_register_indicator_for_bars(py, bar.bar_type, indicator.clone_ref(py));
4211            rust_actor.py_register_indicator_for_bars(py, bar.bar_type, indicator.clone_ref(py));
4212
4213            DataActor::handle_quote(rust_actor.inner_mut(), &quote);
4214            DataActor::handle_trade(rust_actor.inner_mut(), &trade);
4215            DataActor::handle_bar(rust_actor.inner_mut(), &bar);
4216
4217            assert_eq!(
4218                rust_actor
4219                    .py_registered_indicators(py)
4220                    .unwrap()
4221                    .bind(py)
4222                    .len()
4223                    .unwrap(),
4224                1
4225            );
4226            assert_eq!(python_indicator_call_count(&indicator, py, "quote"), 1);
4227            assert_eq!(python_indicator_call_count(&indicator, py, "trade"), 1);
4228            assert_eq!(python_indicator_call_count(&indicator, py, "bar"), 1);
4229        });
4230    }
4231
4232    #[rstest]
4233    fn test_indicator_error_prevents_actor_callback(
4234        clock: Rc<RefCell<TestClock>>,
4235        cache: Rc<RefCell<Cache>>,
4236        trader_id: TraderId,
4237    ) {
4238        pyo3::Python::initialize();
4239
4240        Python::attach(|py| {
4241            let events = PyList::empty(py);
4242            let py_actor = create_indicator_event_actor(py, &events).unwrap();
4243            let indicator = create_raising_python_indicator(py).unwrap();
4244
4245            let mut rust_actor = PyDataActor::new(None);
4246            rust_actor.set_python_instance(py_actor.clone_ref(py));
4247            rust_actor.register(trader_id, clock, cache).unwrap();
4248            Component::start(rust_actor.inner_mut()).unwrap();
4249
4250            let quote = sample_quote();
4251
4252            rust_actor.py_register_indicator_for_quote_ticks(py, quote.instrument_id, indicator);
4253
4254            DataActor::handle_quote(rust_actor.inner_mut(), &quote);
4255            let events = events.extract::<Vec<String>>().unwrap();
4256
4257            assert!(events.is_empty());
4258        });
4259    }
4260
4261    #[rstest]
4262    #[case("on_time_event")]
4263    #[case("on_data")]
4264    #[case("on_signal")]
4265    #[case("on_instrument")]
4266    #[case("on_quote")]
4267    #[case("on_trade")]
4268    #[case("on_bar")]
4269    #[case("on_book")]
4270    #[case("on_book_deltas")]
4271    #[case("on_mark_price")]
4272    #[case("on_index_price")]
4273    #[case("on_funding_rate")]
4274    #[case("on_instrument_status")]
4275    #[case("on_instrument_close")]
4276    #[case("on_option_greeks")]
4277    #[case("on_option_chain")]
4278    fn test_python_dispatch_typed_callback_matrix(
4279        clock: Rc<RefCell<TestClock>>,
4280        cache: Rc<RefCell<Cache>>,
4281        trader_id: TraderId,
4282        #[case] method_name: &str,
4283    ) {
4284        pyo3::Python::initialize();
4285
4286        Python::attach(|py| {
4287            assert_python_dispatch(py, clock, cache, trader_id, method_name, |rust_actor| {
4288                match method_name {
4289                    "on_time_event" => {
4290                        let event = sample_time_event();
4291                        rust_actor.inner_mut().on_time_event(&event)
4292                    }
4293                    "on_data" => {
4294                        let data = sample_data();
4295                        rust_actor.inner_mut().on_data(&data)
4296                    }
4297                    "on_signal" => {
4298                        let signal = sample_signal();
4299                        rust_actor.inner_mut().on_signal(&signal)
4300                    }
4301                    "on_instrument" => {
4302                        let instrument = InstrumentAny::CurrencyPair(sample_instrument());
4303                        rust_actor.inner_mut().on_instrument(&instrument)
4304                    }
4305                    "on_quote" => {
4306                        let quote = sample_quote();
4307                        rust_actor.inner_mut().on_quote(&quote)
4308                    }
4309                    "on_trade" => {
4310                        let trade = sample_trade();
4311                        rust_actor.inner_mut().on_trade(&trade)
4312                    }
4313                    "on_bar" => {
4314                        let bar = sample_bar();
4315                        rust_actor.inner_mut().on_bar(&bar)
4316                    }
4317                    "on_book" => {
4318                        let book = sample_book();
4319                        rust_actor.inner_mut().on_book(&book)
4320                    }
4321                    "on_book_deltas" => {
4322                        let deltas = sample_book_deltas();
4323                        rust_actor.inner_mut().on_book_deltas(&deltas)
4324                    }
4325                    "on_mark_price" => {
4326                        let update = sample_mark_price();
4327                        rust_actor.inner_mut().on_mark_price(&update)
4328                    }
4329                    "on_index_price" => {
4330                        let update = sample_index_price();
4331                        rust_actor.inner_mut().on_index_price(&update)
4332                    }
4333                    "on_funding_rate" => {
4334                        let update = sample_funding_rate();
4335                        rust_actor.inner_mut().on_funding_rate(&update)
4336                    }
4337                    "on_instrument_status" => {
4338                        let status = sample_instrument_status();
4339                        rust_actor.inner_mut().on_instrument_status(&status)
4340                    }
4341                    "on_instrument_close" => {
4342                        let close = sample_instrument_close();
4343                        rust_actor.inner_mut().on_instrument_close(&close)
4344                    }
4345                    "on_option_greeks" => {
4346                        let greeks = sample_option_greeks();
4347                        rust_actor.inner_mut().on_option_greeks(&greeks)
4348                    }
4349                    "on_option_chain" => {
4350                        let chain = sample_option_chain();
4351                        rust_actor.inner_mut().on_option_chain(&chain)
4352                    }
4353                    _ => unreachable!("unhandled typed callback case: {method_name}"),
4354                }
4355            });
4356        });
4357    }
4358
4359    #[rstest]
4360    #[case("on_historical_data")]
4361    #[case("on_historical_quotes")]
4362    #[case("on_historical_trades")]
4363    #[case("on_historical_funding_rates")]
4364    #[case("on_historical_bars")]
4365    #[case("on_historical_mark_prices")]
4366    #[case("on_historical_index_prices")]
4367    fn test_python_dispatch_historical_callback_matrix(
4368        clock: Rc<RefCell<TestClock>>,
4369        cache: Rc<RefCell<Cache>>,
4370        trader_id: TraderId,
4371        #[case] method_name: &str,
4372    ) {
4373        pyo3::Python::initialize();
4374
4375        Python::attach(|py| {
4376            assert_python_dispatch(py, clock, cache, trader_id, method_name, |rust_actor| {
4377                match method_name {
4378                    "on_historical_data" => {
4379                        let data = sample_data();
4380                        rust_actor.inner_mut().on_historical_data(&data)
4381                    }
4382                    "on_historical_quotes" => {
4383                        let quotes = vec![sample_quote()];
4384                        rust_actor.inner_mut().on_historical_quotes(&quotes)
4385                    }
4386                    "on_historical_trades" => {
4387                        let trades = vec![sample_trade()];
4388                        rust_actor.inner_mut().on_historical_trades(&trades)
4389                    }
4390                    "on_historical_funding_rates" => {
4391                        let funding_rates = vec![sample_funding_rate()];
4392                        rust_actor
4393                            .inner_mut()
4394                            .on_historical_funding_rates(&funding_rates)
4395                    }
4396                    "on_historical_bars" => {
4397                        let bars = vec![sample_bar()];
4398                        rust_actor.inner_mut().on_historical_bars(&bars)
4399                    }
4400                    "on_historical_mark_prices" => {
4401                        let mark_prices = vec![sample_mark_price()];
4402                        rust_actor
4403                            .inner_mut()
4404                            .on_historical_mark_prices(&mark_prices)
4405                    }
4406                    "on_historical_index_prices" => {
4407                        let index_prices = vec![sample_index_price()];
4408                        rust_actor
4409                            .inner_mut()
4410                            .on_historical_index_prices(&index_prices)
4411                    }
4412                    _ => unreachable!("unhandled historical callback case: {method_name}"),
4413                }
4414            });
4415        });
4416    }
4417
4418    #[cfg(feature = "defi")]
4419    #[rstest]
4420    #[case("on_block")]
4421    #[case("on_pool")]
4422    #[case("on_pool_swap")]
4423    #[case("on_pool_liquidity_update")]
4424    #[case("on_pool_fee_collect")]
4425    #[case("on_pool_flash")]
4426    fn test_python_dispatch_defi_callback_matrix(
4427        clock: Rc<RefCell<TestClock>>,
4428        cache: Rc<RefCell<Cache>>,
4429        trader_id: TraderId,
4430        #[case] method_name: &str,
4431    ) {
4432        pyo3::Python::initialize();
4433
4434        Python::attach(|py| {
4435            assert_python_dispatch(py, clock, cache, trader_id, method_name, |rust_actor| {
4436                match method_name {
4437                    "on_block" => {
4438                        let block = sample_block();
4439                        rust_actor.inner_mut().on_block(&block)
4440                    }
4441                    "on_pool" => {
4442                        let (_chain, _dex, pool) = sample_pool_components();
4443                        rust_actor.inner_mut().on_pool(&pool)
4444                    }
4445                    "on_pool_swap" => {
4446                        let swap = sample_pool_swap();
4447                        rust_actor.inner_mut().on_pool_swap(&swap)
4448                    }
4449                    "on_pool_liquidity_update" => {
4450                        let update = sample_pool_liquidity_update();
4451                        rust_actor.inner_mut().on_pool_liquidity_update(&update)
4452                    }
4453                    "on_pool_fee_collect" => {
4454                        let collect = sample_pool_fee_collect();
4455                        rust_actor.inner_mut().on_pool_fee_collect(&collect)
4456                    }
4457                    "on_pool_flash" => {
4458                        let flash = sample_pool_flash();
4459                        rust_actor.inner_mut().on_pool_flash(&flash)
4460                    }
4461                    _ => unreachable!("unhandled defi callback case: {method_name}"),
4462                }
4463            });
4464        });
4465    }
4466
4467    #[rstest]
4468    fn test_python_dispatch_multiple_calls_tracked(
4469        clock: Rc<RefCell<TestClock>>,
4470        cache: Rc<RefCell<Cache>>,
4471        trader_id: TraderId,
4472        audusd_sim: CurrencyPair,
4473    ) {
4474        pyo3::Python::initialize();
4475
4476        Python::attach(|py| {
4477            let py_actor = create_tracking_python_actor(py).unwrap();
4478
4479            let mut rust_actor = PyDataActor::new(None);
4480            rust_actor.set_python_instance(py_actor.clone_ref(py));
4481            rust_actor.register(trader_id, clock, cache).unwrap();
4482
4483            let quote = QuoteTick::new(
4484                audusd_sim.id,
4485                Price::from("1.00000"),
4486                Price::from("1.00001"),
4487                Quantity::from(100_000),
4488                Quantity::from(100_000),
4489                UnixNanos::default(),
4490                UnixNanos::default(),
4491            );
4492
4493            rust_actor.inner_mut().on_quote(&quote).unwrap();
4494            rust_actor.inner_mut().on_quote(&quote).unwrap();
4495            rust_actor.inner_mut().on_quote(&quote).unwrap();
4496
4497            assert_eq!(python_method_call_count(&py_actor, py, "on_quote"), 3);
4498        });
4499    }
4500
4501    #[rstest]
4502    fn test_python_dispatch_no_call_when_py_self_not_set(
4503        clock: Rc<RefCell<TestClock>>,
4504        cache: Rc<RefCell<Cache>>,
4505        trader_id: TraderId,
4506    ) {
4507        pyo3::Python::initialize();
4508
4509        Python::attach(|_py| {
4510            let mut rust_actor = PyDataActor::new(None);
4511            rust_actor.register(trader_id, clock, cache).unwrap();
4512
4513            // When py_self is None, the dispatch returns Ok(()) without calling Python
4514            let result = DataActor::on_start(rust_actor.inner_mut());
4515            assert!(result.is_ok());
4516        });
4517    }
4518
4519    #[rstest]
4520    fn test_python_on_historical_data_rejects_non_custom_data(
4521        clock: Rc<RefCell<TestClock>>,
4522        cache: Rc<RefCell<Cache>>,
4523        trader_id: TraderId,
4524    ) {
4525        pyo3::Python::initialize();
4526
4527        let mut rust_actor = PyDataActor::new(None);
4528        rust_actor.register(trader_id, clock, cache).unwrap();
4529
4530        let non_custom: String = "not CustomData".to_string();
4531        let result = rust_actor.inner_mut().on_historical_data(&non_custom);
4532
4533        assert!(result.is_err());
4534        assert!(result.unwrap_err().to_string().contains("unsupported type"));
4535    }
4536}