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