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