Skip to main content

nautilus_trading/python/
strategy.rs

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