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 nautilus_common::{
29    actor::{
30        Actor, DataActor,
31        data_actor::DataActorCore,
32        registry::{try_get_actor_unchecked, with_actor_registry},
33    },
34    cache::Cache,
35    clock::Clock,
36    component::{Component, with_component_registry},
37    enums::ComponentState,
38    python::{cache::PyCache, clock::PyClock, logging::PyLogger},
39    signal::Signal,
40    timer::{TimeEvent, TimeEventCallback},
41};
42use nautilus_core::{
43    Params, from_pydict,
44    nanos::UnixNanos,
45    python::{IntoPyObjectNautilusExt, to_pyruntime_err, to_pyvalue_err},
46};
47use nautilus_model::{
48    data::{
49        Bar, BarType, CustomData, DataType, FundingRateUpdate, IndexPriceUpdate, InstrumentStatus,
50        MarkPriceUpdate, OrderBookDeltas, QuoteTick, TradeTick,
51        close::InstrumentClose,
52        option_chain::{OptionChainSlice, OptionGreeks},
53    },
54    enums::{BookType, OmsType, OrderSide, PositionSide, TimeInForce},
55    events::{
56        OrderAccepted, OrderCancelRejected, OrderCanceled, OrderDenied, OrderEmulated,
57        OrderExpired, OrderFilled, OrderInitialized, OrderModifyRejected, OrderPendingCancel,
58        OrderPendingUpdate, OrderRejected, OrderReleased, OrderSubmitted, OrderTriggered,
59        OrderUpdated, PositionChanged, PositionClosed, PositionOpened,
60    },
61    identifiers::{
62        AccountId, ClientId, ClientOrderId, InstrumentId, OptionSeriesId, PositionId, StrategyId,
63        TraderId, Venue,
64    },
65    instruments::InstrumentAny,
66    orderbook::OrderBook,
67    position::Position,
68    python::{
69        data::option_chain::PyStrikeRange, instruments::instrument_any_to_pyobject,
70        orders::pyobject_to_order_any,
71    },
72    types::{Price, Quantity},
73};
74use nautilus_portfolio::{portfolio::Portfolio, python::PyPortfolio};
75use pyo3::{prelude::*, types::PyDict};
76use ustr::Ustr;
77
78use crate::strategy::{ImportableStrategyConfig, Strategy, StrategyConfig, StrategyCore};
79
80#[pyo3::pymethods]
81#[pyo3_stub_gen::derive::gen_stub_pymethods]
82impl StrategyConfig {
83    /// The base model for all trading strategy configurations.
84    #[new]
85    #[pyo3(signature = (
86        strategy_id=None,
87        order_id_tag=None,
88        oms_type=None,
89        external_order_claims=None,
90        manage_contingent_orders=false,
91        manage_gtd_expiry=false,
92        manage_stop=false,
93        market_exit_interval_ms=100,
94        market_exit_max_attempts=100,
95        market_exit_time_in_force=TimeInForce::Gtc,
96        market_exit_reduce_only=true,
97        use_uuid_client_order_ids=false,
98        use_hyphens_in_client_order_ids=true,
99        log_events=true,
100        log_commands=true,
101        log_rejected_due_post_only_as_warning=true,
102        **_kwargs
103    ))]
104    #[expect(clippy::too_many_arguments)]
105    fn py_new(
106        strategy_id: Option<StrategyId>,
107        order_id_tag: Option<String>,
108        oms_type: Option<OmsType>,
109        external_order_claims: Option<Vec<InstrumentId>>,
110        manage_contingent_orders: bool,
111        manage_gtd_expiry: bool,
112        manage_stop: bool,
113        market_exit_interval_ms: u64,
114        market_exit_max_attempts: u64,
115        market_exit_time_in_force: TimeInForce,
116        market_exit_reduce_only: bool,
117        use_uuid_client_order_ids: bool,
118        use_hyphens_in_client_order_ids: bool,
119        log_events: bool,
120        log_commands: bool,
121        log_rejected_due_post_only_as_warning: bool,
122        _kwargs: Option<&Bound<'_, PyDict>>,
123    ) -> Self {
124        Self {
125            strategy_id,
126            order_id_tag,
127            use_uuid_client_order_ids,
128            use_hyphens_in_client_order_ids,
129            oms_type,
130            external_order_claims,
131            manage_contingent_orders,
132            manage_gtd_expiry,
133            manage_stop,
134            market_exit_interval_ms,
135            market_exit_max_attempts,
136            market_exit_time_in_force,
137            market_exit_reduce_only,
138            log_events,
139            log_commands,
140            log_rejected_due_post_only_as_warning,
141        }
142    }
143
144    #[getter]
145    fn strategy_id(&self) -> Option<StrategyId> {
146        self.strategy_id
147    }
148
149    #[getter]
150    fn order_id_tag(&self) -> Option<&String> {
151        self.order_id_tag.as_ref()
152    }
153
154    #[getter]
155    fn oms_type(&self) -> Option<OmsType> {
156        self.oms_type
157    }
158
159    #[getter]
160    fn manage_contingent_orders(&self) -> bool {
161        self.manage_contingent_orders
162    }
163
164    #[getter]
165    fn manage_gtd_expiry(&self) -> bool {
166        self.manage_gtd_expiry
167    }
168
169    #[getter]
170    fn use_uuid_client_order_ids(&self) -> bool {
171        self.use_uuid_client_order_ids
172    }
173
174    #[getter]
175    fn use_hyphens_in_client_order_ids(&self) -> bool {
176        self.use_hyphens_in_client_order_ids
177    }
178
179    #[getter]
180    fn log_events(&self) -> bool {
181        self.log_events
182    }
183
184    #[getter]
185    fn log_commands(&self) -> bool {
186        self.log_commands
187    }
188
189    #[getter]
190    fn log_rejected_due_post_only_as_warning(&self) -> bool {
191        self.log_rejected_due_post_only_as_warning
192    }
193}
194
195#[pyo3::pymethods]
196#[pyo3_stub_gen::derive::gen_stub_pymethods]
197impl ImportableStrategyConfig {
198    /// Configuration for creating strategies from importable paths.
199    #[new]
200    #[expect(clippy::needless_pass_by_value)]
201    fn py_new(strategy_path: String, config_path: String, config: Py<PyDict>) -> PyResult<Self> {
202        let json_config = Python::attach(|py| -> PyResult<HashMap<String, serde_json::Value>> {
203            let kwargs = PyDict::new(py);
204            kwargs.set_item("default", py.eval(pyo3::ffi::c_str!("str"), None, None)?)?;
205            let json_str: String = PyModule::import(py, "json")?
206                .call_method("dumps", (config.bind(py),), Some(&kwargs))?
207                .extract()?;
208
209            let json_value: serde_json::Value =
210                serde_json::from_str(&json_str).map_err(to_pyvalue_err)?;
211
212            if let serde_json::Value::Object(map) = json_value {
213                Ok(map.into_iter().collect())
214            } else {
215                Err(to_pyvalue_err("Config must be a dictionary"))
216            }
217        })?;
218
219        Ok(Self {
220            strategy_path,
221            config_path,
222            config: json_config,
223        })
224    }
225
226    #[getter]
227    fn strategy_path(&self) -> &String {
228        &self.strategy_path
229    }
230
231    #[getter]
232    fn config_path(&self) -> &String {
233        &self.config_path
234    }
235
236    #[getter]
237    fn config(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
238        let py_dict = PyDict::new(py);
239
240        for (key, value) in &self.config {
241            let json_str = serde_json::to_string(value).map_err(to_pyvalue_err)?;
242            let py_value = PyModule::import(py, "json")?.call_method("loads", (json_str,), None)?;
243            py_dict.set_item(key, py_value)?;
244        }
245        Ok(py_dict.unbind())
246    }
247}
248
249/// Inner state of PyStrategy, shared between Python wrapper and Rust registries.
250pub struct PyStrategyInner {
251    core: StrategyCore,
252    py_self: Option<Py<PyAny>>,
253    clock: PyClock,
254    logger: PyLogger,
255}
256
257impl Debug for PyStrategyInner {
258    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259        f.debug_struct(stringify!(PyStrategyInner))
260            .field("core", &self.core)
261            .field("py_self", &self.py_self.as_ref().map(|_| "<Py<PyAny>>"))
262            .field("clock", &self.clock)
263            .field("logger", &self.logger)
264            .finish()
265    }
266}
267
268#[expect(clippy::needless_pass_by_ref_mut)]
269impl PyStrategyInner {
270    fn dispatch_on_start(&self) -> PyResult<()> {
271        if let Some(ref py_self) = self.py_self {
272            Python::attach(|py| py_self.call_method0(py, "on_start"))?;
273        }
274        Ok(())
275    }
276
277    fn dispatch_on_stop(&self) -> PyResult<()> {
278        if let Some(ref py_self) = self.py_self {
279            Python::attach(|py| py_self.call_method0(py, "on_stop"))?;
280        }
281        Ok(())
282    }
283
284    fn dispatch_on_resume(&self) -> PyResult<()> {
285        if let Some(ref py_self) = self.py_self {
286            Python::attach(|py| py_self.call_method0(py, "on_resume"))?;
287        }
288        Ok(())
289    }
290
291    fn dispatch_on_reset(&self) -> PyResult<()> {
292        if let Some(ref py_self) = self.py_self {
293            Python::attach(|py| py_self.call_method0(py, "on_reset"))?;
294        }
295        Ok(())
296    }
297
298    fn dispatch_on_dispose(&self) -> PyResult<()> {
299        if let Some(ref py_self) = self.py_self {
300            Python::attach(|py| py_self.call_method0(py, "on_dispose"))?;
301        }
302        Ok(())
303    }
304
305    fn dispatch_on_degrade(&self) -> PyResult<()> {
306        if let Some(ref py_self) = self.py_self {
307            Python::attach(|py| py_self.call_method0(py, "on_degrade"))?;
308        }
309        Ok(())
310    }
311
312    fn dispatch_on_fault(&self) -> PyResult<()> {
313        if let Some(ref py_self) = self.py_self {
314            Python::attach(|py| py_self.call_method0(py, "on_fault"))?;
315        }
316        Ok(())
317    }
318
319    fn dispatch_on_time_event(&self, event: &TimeEvent) -> PyResult<()> {
320        if let Some(ref py_self) = self.py_self {
321            Python::attach(|py| {
322                py_self.call_method1(py, "on_time_event", (event.clone().into_py_any_unwrap(py),))
323            })?;
324        }
325        Ok(())
326    }
327
328    fn dispatch_on_order_initialized(&self, event: OrderInitialized) -> PyResult<()> {
329        if let Some(ref py_self) = self.py_self {
330            Python::attach(|py| {
331                py_self.call_method1(py, "on_order_initialized", (event.into_py_any_unwrap(py),))
332            })?;
333        }
334        Ok(())
335    }
336
337    fn dispatch_on_order_denied(&self, event: OrderDenied) -> PyResult<()> {
338        if let Some(ref py_self) = self.py_self {
339            Python::attach(|py| {
340                py_self.call_method1(py, "on_order_denied", (event.into_py_any_unwrap(py),))
341            })?;
342        }
343        Ok(())
344    }
345
346    fn dispatch_on_order_emulated(&self, event: OrderEmulated) -> PyResult<()> {
347        if let Some(ref py_self) = self.py_self {
348            Python::attach(|py| {
349                py_self.call_method1(py, "on_order_emulated", (event.into_py_any_unwrap(py),))
350            })?;
351        }
352        Ok(())
353    }
354
355    fn dispatch_on_order_released(&self, event: OrderReleased) -> PyResult<()> {
356        if let Some(ref py_self) = self.py_self {
357            Python::attach(|py| {
358                py_self.call_method1(py, "on_order_released", (event.into_py_any_unwrap(py),))
359            })?;
360        }
361        Ok(())
362    }
363
364    fn dispatch_on_order_submitted(&self, event: OrderSubmitted) -> PyResult<()> {
365        if let Some(ref py_self) = self.py_self {
366            Python::attach(|py| {
367                py_self.call_method1(py, "on_order_submitted", (event.into_py_any_unwrap(py),))
368            })?;
369        }
370        Ok(())
371    }
372
373    fn dispatch_on_order_rejected(&self, event: OrderRejected) -> PyResult<()> {
374        if let Some(ref py_self) = self.py_self {
375            Python::attach(|py| {
376                py_self.call_method1(py, "on_order_rejected", (event.into_py_any_unwrap(py),))
377            })?;
378        }
379        Ok(())
380    }
381
382    fn dispatch_on_order_accepted(&self, event: OrderAccepted) -> PyResult<()> {
383        if let Some(ref py_self) = self.py_self {
384            Python::attach(|py| {
385                py_self.call_method1(py, "on_order_accepted", (event.into_py_any_unwrap(py),))
386            })?;
387        }
388        Ok(())
389    }
390
391    fn dispatch_on_order_expired(&self, event: OrderExpired) -> PyResult<()> {
392        if let Some(ref py_self) = self.py_self {
393            Python::attach(|py| {
394                py_self.call_method1(py, "on_order_expired", (event.into_py_any_unwrap(py),))
395            })?;
396        }
397        Ok(())
398    }
399
400    fn dispatch_on_order_triggered(&self, event: OrderTriggered) -> PyResult<()> {
401        if let Some(ref py_self) = self.py_self {
402            Python::attach(|py| {
403                py_self.call_method1(py, "on_order_triggered", (event.into_py_any_unwrap(py),))
404            })?;
405        }
406        Ok(())
407    }
408
409    fn dispatch_on_order_pending_update(&self, event: OrderPendingUpdate) -> PyResult<()> {
410        if let Some(ref py_self) = self.py_self {
411            Python::attach(|py| {
412                py_self.call_method1(
413                    py,
414                    "on_order_pending_update",
415                    (event.into_py_any_unwrap(py),),
416                )
417            })?;
418        }
419        Ok(())
420    }
421
422    fn dispatch_on_order_pending_cancel(&self, event: OrderPendingCancel) -> PyResult<()> {
423        if let Some(ref py_self) = self.py_self {
424            Python::attach(|py| {
425                py_self.call_method1(
426                    py,
427                    "on_order_pending_cancel",
428                    (event.into_py_any_unwrap(py),),
429                )
430            })?;
431        }
432        Ok(())
433    }
434
435    fn dispatch_on_order_modify_rejected(&self, event: OrderModifyRejected) -> PyResult<()> {
436        if let Some(ref py_self) = self.py_self {
437            Python::attach(|py| {
438                py_self.call_method1(
439                    py,
440                    "on_order_modify_rejected",
441                    (event.into_py_any_unwrap(py),),
442                )
443            })?;
444        }
445        Ok(())
446    }
447
448    fn dispatch_on_order_cancel_rejected(&self, event: OrderCancelRejected) -> PyResult<()> {
449        if let Some(ref py_self) = self.py_self {
450            Python::attach(|py| {
451                py_self.call_method1(
452                    py,
453                    "on_order_cancel_rejected",
454                    (event.into_py_any_unwrap(py),),
455                )
456            })?;
457        }
458        Ok(())
459    }
460
461    fn dispatch_on_order_updated(&self, event: OrderUpdated) -> PyResult<()> {
462        if let Some(ref py_self) = self.py_self {
463            Python::attach(|py| {
464                py_self.call_method1(py, "on_order_updated", (event.into_py_any_unwrap(py),))
465            })?;
466        }
467        Ok(())
468    }
469
470    fn dispatch_on_order_canceled(&self, event: OrderCanceled) -> PyResult<()> {
471        if let Some(ref py_self) = self.py_self {
472            Python::attach(|py| {
473                py_self.call_method1(py, "on_order_canceled", (event.into_py_any_unwrap(py),))
474            })?;
475        }
476        Ok(())
477    }
478
479    fn dispatch_on_order_filled(&self, event: OrderFilled) -> PyResult<()> {
480        if let Some(ref py_self) = self.py_self {
481            Python::attach(|py| {
482                py_self.call_method1(py, "on_order_filled", (event.into_py_any_unwrap(py),))
483            })?;
484        }
485        Ok(())
486    }
487
488    fn dispatch_on_position_opened(&self, event: PositionOpened) -> PyResult<()> {
489        if let Some(ref py_self) = self.py_self {
490            Python::attach(|py| {
491                py_self.call_method1(py, "on_position_opened", (event.into_py_any_unwrap(py),))
492            })?;
493        }
494        Ok(())
495    }
496
497    fn dispatch_on_position_changed(&self, event: PositionChanged) -> PyResult<()> {
498        if let Some(ref py_self) = self.py_self {
499            Python::attach(|py| {
500                py_self.call_method1(py, "on_position_changed", (event.into_py_any_unwrap(py),))
501            })?;
502        }
503        Ok(())
504    }
505
506    fn dispatch_on_position_closed(&self, event: PositionClosed) -> PyResult<()> {
507        if let Some(ref py_self) = self.py_self {
508            Python::attach(|py| {
509                py_self.call_method1(py, "on_position_closed", (event.into_py_any_unwrap(py),))
510            })?;
511        }
512        Ok(())
513    }
514
515    fn dispatch_on_data(&mut self, data: Py<PyAny>) -> PyResult<()> {
516        if let Some(ref py_self) = self.py_self {
517            Python::attach(|py| py_self.call_method1(py, "on_data", (data,)))?;
518        }
519        Ok(())
520    }
521
522    fn dispatch_on_signal(&mut self, signal: &Signal) -> PyResult<()> {
523        if let Some(ref py_self) = self.py_self {
524            Python::attach(|py| {
525                py_self.call_method1(py, "on_signal", (signal.clone().into_py_any_unwrap(py),))
526            })?;
527        }
528        Ok(())
529    }
530
531    fn dispatch_on_instrument(&mut self, instrument: Py<PyAny>) -> PyResult<()> {
532        if let Some(ref py_self) = self.py_self {
533            Python::attach(|py| py_self.call_method1(py, "on_instrument", (instrument,)))?;
534        }
535        Ok(())
536    }
537
538    fn dispatch_on_quote(&mut self, quote: QuoteTick) -> PyResult<()> {
539        if let Some(ref py_self) = self.py_self {
540            Python::attach(|py| {
541                py_self.call_method1(py, "on_quote", (quote.into_py_any_unwrap(py),))
542            })?;
543        }
544        Ok(())
545    }
546
547    fn dispatch_on_trade(&mut self, trade: TradeTick) -> PyResult<()> {
548        if let Some(ref py_self) = self.py_self {
549            Python::attach(|py| {
550                py_self.call_method1(py, "on_trade", (trade.into_py_any_unwrap(py),))
551            })?;
552        }
553        Ok(())
554    }
555
556    fn dispatch_on_bar(&mut self, bar: Bar) -> PyResult<()> {
557        if let Some(ref py_self) = self.py_self {
558            Python::attach(|py| py_self.call_method1(py, "on_bar", (bar.into_py_any_unwrap(py),)))?;
559        }
560        Ok(())
561    }
562
563    fn dispatch_on_book_deltas(&mut self, deltas: OrderBookDeltas) -> PyResult<()> {
564        if let Some(ref py_self) = self.py_self {
565            Python::attach(|py| {
566                py_self.call_method1(py, "on_book_deltas", (deltas.into_py_any_unwrap(py),))
567            })?;
568        }
569        Ok(())
570    }
571
572    fn dispatch_on_book(&mut self, book: &OrderBook) -> PyResult<()> {
573        if let Some(ref py_self) = self.py_self {
574            Python::attach(|py| {
575                py_self.call_method1(py, "on_book", (book.clone().into_py_any_unwrap(py),))
576            })?;
577        }
578        Ok(())
579    }
580
581    fn dispatch_on_mark_price(&mut self, mark_price: MarkPriceUpdate) -> PyResult<()> {
582        if let Some(ref py_self) = self.py_self {
583            Python::attach(|py| {
584                py_self.call_method1(py, "on_mark_price", (mark_price.into_py_any_unwrap(py),))
585            })?;
586        }
587        Ok(())
588    }
589
590    fn dispatch_on_index_price(&mut self, index_price: IndexPriceUpdate) -> PyResult<()> {
591        if let Some(ref py_self) = self.py_self {
592            Python::attach(|py| {
593                py_self.call_method1(py, "on_index_price", (index_price.into_py_any_unwrap(py),))
594            })?;
595        }
596        Ok(())
597    }
598
599    fn dispatch_on_funding_rate(&mut self, funding_rate: FundingRateUpdate) -> PyResult<()> {
600        if let Some(ref py_self) = self.py_self {
601            Python::attach(|py| {
602                py_self.call_method1(
603                    py,
604                    "on_funding_rate",
605                    (funding_rate.into_py_any_unwrap(py),),
606                )
607            })?;
608        }
609        Ok(())
610    }
611
612    fn dispatch_on_instrument_status(&mut self, data: InstrumentStatus) -> PyResult<()> {
613        if let Some(ref py_self) = self.py_self {
614            Python::attach(|py| {
615                py_self.call_method1(py, "on_instrument_status", (data.into_py_any_unwrap(py),))
616            })?;
617        }
618        Ok(())
619    }
620
621    fn dispatch_on_instrument_close(&mut self, update: InstrumentClose) -> PyResult<()> {
622        if let Some(ref py_self) = self.py_self {
623            Python::attach(|py| {
624                py_self.call_method1(py, "on_instrument_close", (update.into_py_any_unwrap(py),))
625            })?;
626        }
627        Ok(())
628    }
629
630    fn dispatch_on_option_greeks(&mut self, greeks: OptionGreeks) -> PyResult<()> {
631        if let Some(ref py_self) = self.py_self {
632            Python::attach(|py| {
633                py_self.call_method1(py, "on_option_greeks", (greeks.into_py_any_unwrap(py),))
634            })?;
635        }
636        Ok(())
637    }
638
639    fn dispatch_on_option_chain(&mut self, slice: OptionChainSlice) -> PyResult<()> {
640        if let Some(ref py_self) = self.py_self {
641            Python::attach(|py| {
642                py_self.call_method1(py, "on_option_chain", (slice.into_py_any_unwrap(py),))
643            })?;
644        }
645        Ok(())
646    }
647
648    fn dispatch_on_historical_data(&mut self, data: Py<PyAny>) -> PyResult<()> {
649        if let Some(ref py_self) = self.py_self {
650            Python::attach(|py| py_self.call_method1(py, "on_historical_data", (data,)))?;
651        }
652        Ok(())
653    }
654
655    fn dispatch_on_historical_quotes(&mut self, quotes: Vec<QuoteTick>) -> PyResult<()> {
656        if let Some(ref py_self) = self.py_self {
657            Python::attach(|py| {
658                let py_quotes: Vec<_> = quotes
659                    .into_iter()
660                    .map(|quote| quote.into_py_any_unwrap(py))
661                    .collect();
662                py_self.call_method1(py, "on_historical_quotes", (py_quotes,))
663            })?;
664        }
665        Ok(())
666    }
667
668    fn dispatch_on_historical_trades(&mut self, trades: Vec<TradeTick>) -> PyResult<()> {
669        if let Some(ref py_self) = self.py_self {
670            Python::attach(|py| {
671                let py_trades: Vec<_> = trades
672                    .into_iter()
673                    .map(|trade| trade.into_py_any_unwrap(py))
674                    .collect();
675                py_self.call_method1(py, "on_historical_trades", (py_trades,))
676            })?;
677        }
678        Ok(())
679    }
680
681    fn dispatch_on_historical_funding_rates(
682        &mut self,
683        funding_rates: Vec<FundingRateUpdate>,
684    ) -> PyResult<()> {
685        if let Some(ref py_self) = self.py_self {
686            Python::attach(|py| {
687                let py_funding_rates: Vec<_> = funding_rates
688                    .into_iter()
689                    .map(|rate| rate.into_py_any_unwrap(py))
690                    .collect();
691                py_self.call_method1(py, "on_historical_funding_rates", (py_funding_rates,))
692            })?;
693        }
694        Ok(())
695    }
696
697    fn dispatch_on_historical_bars(&mut self, bars: Vec<Bar>) -> PyResult<()> {
698        if let Some(ref py_self) = self.py_self {
699            Python::attach(|py| {
700                let py_bars: Vec<_> = bars
701                    .into_iter()
702                    .map(|bar| bar.into_py_any_unwrap(py))
703                    .collect();
704                py_self.call_method1(py, "on_historical_bars", (py_bars,))
705            })?;
706        }
707        Ok(())
708    }
709
710    fn dispatch_on_historical_mark_prices(
711        &mut self,
712        mark_prices: Vec<MarkPriceUpdate>,
713    ) -> PyResult<()> {
714        if let Some(ref py_self) = self.py_self {
715            Python::attach(|py| {
716                let py_mark_prices: Vec<_> = mark_prices
717                    .into_iter()
718                    .map(|price| price.into_py_any_unwrap(py))
719                    .collect();
720                py_self.call_method1(py, "on_historical_mark_prices", (py_mark_prices,))
721            })?;
722        }
723        Ok(())
724    }
725
726    fn dispatch_on_historical_index_prices(
727        &mut self,
728        index_prices: Vec<IndexPriceUpdate>,
729    ) -> PyResult<()> {
730        if let Some(ref py_self) = self.py_self {
731            Python::attach(|py| {
732                let py_index_prices: Vec<_> = index_prices
733                    .into_iter()
734                    .map(|price| price.into_py_any_unwrap(py))
735                    .collect();
736                py_self.call_method1(py, "on_historical_index_prices", (py_index_prices,))
737            })?;
738        }
739        Ok(())
740    }
741}
742
743impl Deref for PyStrategyInner {
744    type Target = DataActorCore;
745
746    fn deref(&self) -> &Self::Target {
747        &self.core
748    }
749}
750
751impl DerefMut for PyStrategyInner {
752    fn deref_mut(&mut self) -> &mut Self::Target {
753        &mut self.core
754    }
755}
756
757impl Strategy for PyStrategyInner {
758    fn core(&self) -> &StrategyCore {
759        &self.core
760    }
761
762    fn core_mut(&mut self) -> &mut StrategyCore {
763        &mut self.core
764    }
765
766    fn on_order_initialized(&mut self, event: OrderInitialized) {
767        let _ = self.dispatch_on_order_initialized(event);
768    }
769
770    fn on_order_denied(&mut self, event: OrderDenied) {
771        let _ = self.dispatch_on_order_denied(event);
772    }
773
774    fn on_order_emulated(&mut self, event: OrderEmulated) {
775        let _ = self.dispatch_on_order_emulated(event);
776    }
777
778    fn on_order_released(&mut self, event: OrderReleased) {
779        let _ = self.dispatch_on_order_released(event);
780    }
781
782    fn on_order_submitted(&mut self, event: OrderSubmitted) {
783        let _ = self.dispatch_on_order_submitted(event);
784    }
785
786    fn on_order_rejected(&mut self, event: OrderRejected) {
787        let _ = self.dispatch_on_order_rejected(event);
788    }
789
790    fn on_order_accepted(&mut self, event: OrderAccepted) {
791        let _ = self.dispatch_on_order_accepted(event);
792    }
793
794    fn on_order_expired(&mut self, event: OrderExpired) {
795        let _ = self.dispatch_on_order_expired(event);
796    }
797
798    fn on_order_triggered(&mut self, event: OrderTriggered) {
799        let _ = self.dispatch_on_order_triggered(event);
800    }
801
802    fn on_order_pending_update(&mut self, event: OrderPendingUpdate) {
803        let _ = self.dispatch_on_order_pending_update(event);
804    }
805
806    fn on_order_pending_cancel(&mut self, event: OrderPendingCancel) {
807        let _ = self.dispatch_on_order_pending_cancel(event);
808    }
809
810    fn on_order_modify_rejected(&mut self, event: OrderModifyRejected) {
811        let _ = self.dispatch_on_order_modify_rejected(event);
812    }
813
814    fn on_order_cancel_rejected(&mut self, event: OrderCancelRejected) {
815        let _ = self.dispatch_on_order_cancel_rejected(event);
816    }
817
818    fn on_order_updated(&mut self, event: OrderUpdated) {
819        let _ = self.dispatch_on_order_updated(event);
820    }
821
822    fn on_position_opened(&mut self, event: PositionOpened) {
823        let _ = self.dispatch_on_position_opened(event);
824    }
825
826    fn on_position_changed(&mut self, event: PositionChanged) {
827        let _ = self.dispatch_on_position_changed(event);
828    }
829
830    fn on_position_closed(&mut self, event: PositionClosed) {
831        let _ = self.dispatch_on_position_closed(event);
832    }
833}
834
835impl DataActor for PyStrategyInner {
836    fn on_start(&mut self) -> anyhow::Result<()> {
837        Strategy::on_start(self)?;
838        self.dispatch_on_start()
839            .map_err(|e| anyhow::anyhow!("Python on_start failed: {e}"))
840    }
841
842    fn on_stop(&mut self) -> anyhow::Result<()> {
843        self.dispatch_on_stop()
844            .map_err(|e| anyhow::anyhow!("Python on_stop failed: {e}"))
845    }
846
847    fn on_resume(&mut self) -> anyhow::Result<()> {
848        self.dispatch_on_resume()
849            .map_err(|e| anyhow::anyhow!("Python on_resume failed: {e}"))
850    }
851
852    fn on_reset(&mut self) -> anyhow::Result<()> {
853        self.dispatch_on_reset()
854            .map_err(|e| anyhow::anyhow!("Python on_reset failed: {e}"))
855    }
856
857    fn on_dispose(&mut self) -> anyhow::Result<()> {
858        self.dispatch_on_dispose()
859            .map_err(|e| anyhow::anyhow!("Python on_dispose failed: {e}"))
860    }
861
862    fn on_degrade(&mut self) -> anyhow::Result<()> {
863        self.dispatch_on_degrade()
864            .map_err(|e| anyhow::anyhow!("Python on_degrade failed: {e}"))
865    }
866
867    fn on_fault(&mut self) -> anyhow::Result<()> {
868        self.dispatch_on_fault()
869            .map_err(|e| anyhow::anyhow!("Python on_fault failed: {e}"))
870    }
871
872    fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()> {
873        Strategy::on_time_event(self, event)?;
874        self.dispatch_on_time_event(event)
875            .map_err(|e| anyhow::anyhow!("Python on_time_event failed: {e}"))
876    }
877
878    #[allow(unused_variables)]
879    fn on_data(&mut self, data: &CustomData) -> anyhow::Result<()> {
880        Python::attach(|py| {
881            let py_data: Py<PyAny> = Py::new(py, data.clone())?.into_any();
882            self.dispatch_on_data(py_data)
883                .map_err(|e| anyhow::anyhow!("Python on_data failed: {e}"))
884        })
885    }
886
887    fn on_signal(&mut self, signal: &Signal) -> anyhow::Result<()> {
888        self.dispatch_on_signal(signal)
889            .map_err(|e| anyhow::anyhow!("Python on_signal failed: {e}"))
890    }
891
892    fn on_instrument(&mut self, instrument: &InstrumentAny) -> anyhow::Result<()> {
893        Python::attach(|py| {
894            let py_instrument = instrument_any_to_pyobject(py, instrument.clone())
895                .map_err(|e| anyhow::anyhow!("Failed to convert InstrumentAny to Python: {e}"))?;
896            self.dispatch_on_instrument(py_instrument)
897                .map_err(|e| anyhow::anyhow!("Python on_instrument failed: {e}"))
898        })
899    }
900
901    fn on_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
902        self.dispatch_on_quote(*quote)
903            .map_err(|e| anyhow::anyhow!("Python on_quote failed: {e}"))
904    }
905
906    fn on_trade(&mut self, tick: &TradeTick) -> anyhow::Result<()> {
907        self.dispatch_on_trade(*tick)
908            .map_err(|e| anyhow::anyhow!("Python on_trade failed: {e}"))
909    }
910
911    fn on_bar(&mut self, bar: &Bar) -> anyhow::Result<()> {
912        self.dispatch_on_bar(*bar)
913            .map_err(|e| anyhow::anyhow!("Python on_bar failed: {e}"))
914    }
915
916    fn on_book_deltas(&mut self, deltas: &OrderBookDeltas) -> anyhow::Result<()> {
917        self.dispatch_on_book_deltas(deltas.clone())
918            .map_err(|e| anyhow::anyhow!("Python on_book_deltas failed: {e}"))
919    }
920
921    fn on_book(&mut self, order_book: &OrderBook) -> anyhow::Result<()> {
922        self.dispatch_on_book(order_book)
923            .map_err(|e| anyhow::anyhow!("Python on_book failed: {e}"))
924    }
925
926    fn on_mark_price(&mut self, mark_price: &MarkPriceUpdate) -> anyhow::Result<()> {
927        self.dispatch_on_mark_price(*mark_price)
928            .map_err(|e| anyhow::anyhow!("Python on_mark_price failed: {e}"))
929    }
930
931    fn on_index_price(&mut self, index_price: &IndexPriceUpdate) -> anyhow::Result<()> {
932        self.dispatch_on_index_price(*index_price)
933            .map_err(|e| anyhow::anyhow!("Python on_index_price failed: {e}"))
934    }
935
936    fn on_funding_rate(&mut self, funding_rate: &FundingRateUpdate) -> anyhow::Result<()> {
937        self.dispatch_on_funding_rate(*funding_rate)
938            .map_err(|e| anyhow::anyhow!("Python on_funding_rate failed: {e}"))
939    }
940
941    fn on_instrument_status(&mut self, data: &InstrumentStatus) -> anyhow::Result<()> {
942        self.dispatch_on_instrument_status(*data)
943            .map_err(|e| anyhow::anyhow!("Python on_instrument_status failed: {e}"))
944    }
945
946    fn on_instrument_close(&mut self, update: &InstrumentClose) -> anyhow::Result<()> {
947        self.dispatch_on_instrument_close(*update)
948            .map_err(|e| anyhow::anyhow!("Python on_instrument_close failed: {e}"))
949    }
950
951    fn on_option_greeks(&mut self, greeks: &OptionGreeks) -> anyhow::Result<()> {
952        self.dispatch_on_option_greeks(*greeks)
953            .map_err(|e| anyhow::anyhow!("Python on_option_greeks failed: {e}"))
954    }
955
956    fn on_option_chain(&mut self, slice: &OptionChainSlice) -> anyhow::Result<()> {
957        self.dispatch_on_option_chain(slice.clone())
958            .map_err(|e| anyhow::anyhow!("Python on_option_chain failed: {e}"))
959    }
960
961    fn on_historical_data(&mut self, data: &dyn Any) -> anyhow::Result<()> {
962        Python::attach(|py| {
963            let py_data: Py<PyAny> = if let Some(custom_data) = data.downcast_ref::<CustomData>() {
964                Py::new(py, custom_data.clone())?.into_any()
965            } else {
966                anyhow::bail!("Failed to convert historical data to Python: unsupported type");
967            };
968            self.dispatch_on_historical_data(py_data)
969                .map_err(|e| anyhow::anyhow!("Python on_historical_data failed: {e}"))
970        })
971    }
972
973    fn on_historical_quotes(&mut self, quotes: &[QuoteTick]) -> anyhow::Result<()> {
974        self.dispatch_on_historical_quotes(quotes.to_vec())
975            .map_err(|e| anyhow::anyhow!("Python on_historical_quotes failed: {e}"))
976    }
977
978    fn on_historical_trades(&mut self, trades: &[TradeTick]) -> anyhow::Result<()> {
979        self.dispatch_on_historical_trades(trades.to_vec())
980            .map_err(|e| anyhow::anyhow!("Python on_historical_trades failed: {e}"))
981    }
982
983    fn on_historical_funding_rates(
984        &mut self,
985        funding_rates: &[FundingRateUpdate],
986    ) -> anyhow::Result<()> {
987        self.dispatch_on_historical_funding_rates(funding_rates.to_vec())
988            .map_err(|e| anyhow::anyhow!("Python on_historical_funding_rates failed: {e}"))
989    }
990
991    fn on_historical_bars(&mut self, bars: &[Bar]) -> anyhow::Result<()> {
992        self.dispatch_on_historical_bars(bars.to_vec())
993            .map_err(|e| anyhow::anyhow!("Python on_historical_bars failed: {e}"))
994    }
995
996    fn on_historical_mark_prices(&mut self, mark_prices: &[MarkPriceUpdate]) -> anyhow::Result<()> {
997        self.dispatch_on_historical_mark_prices(mark_prices.to_vec())
998            .map_err(|e| anyhow::anyhow!("Python on_historical_mark_prices failed: {e}"))
999    }
1000
1001    fn on_historical_index_prices(
1002        &mut self,
1003        index_prices: &[IndexPriceUpdate],
1004    ) -> anyhow::Result<()> {
1005        self.dispatch_on_historical_index_prices(index_prices.to_vec())
1006            .map_err(|e| anyhow::anyhow!("Python on_historical_index_prices failed: {e}"))
1007    }
1008
1009    fn on_order_filled(&mut self, event: &OrderFilled) -> anyhow::Result<()> {
1010        self.dispatch_on_order_filled(*event)
1011            .map_err(|e| anyhow::anyhow!("Python on_order_filled failed: {e}"))
1012    }
1013
1014    fn on_order_canceled(&mut self, event: &OrderCanceled) -> anyhow::Result<()> {
1015        self.dispatch_on_order_canceled(*event)
1016            .map_err(|e| anyhow::anyhow!("Python on_order_canceled failed: {e}"))
1017    }
1018}
1019
1020/// Python-facing wrapper for Strategy.
1021#[allow(non_camel_case_types)]
1022#[pyo3::pyclass(
1023    module = "nautilus_trader.trading",
1024    name = "Strategy",
1025    unsendable,
1026    subclass
1027)]
1028#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.trading")]
1029pub struct PyStrategy {
1030    inner: Rc<UnsafeCell<PyStrategyInner>>,
1031}
1032
1033impl Debug for PyStrategy {
1034    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1035        f.debug_struct(stringify!(PyStrategy))
1036            .field("inner", &self.inner())
1037            .finish()
1038    }
1039}
1040
1041impl PyStrategy {
1042    #[inline]
1043    #[allow(unsafe_code)]
1044    pub(crate) fn inner(&self) -> &PyStrategyInner {
1045        // SAFETY: `PyStrategy` is `unsendable` so access is single-threaded, and
1046        // callers never hold a mutable and shared reference simultaneously.
1047        unsafe { &*self.inner.get() }
1048    }
1049
1050    #[inline]
1051    #[allow(unsafe_code, clippy::mut_from_ref)]
1052    pub(crate) fn inner_mut(&self) -> &mut PyStrategyInner {
1053        // SAFETY: `PyStrategy` is `unsendable` so access is single-threaded, and
1054        // callers never hold a mutable and shared reference simultaneously.
1055        unsafe { &mut *self.inner.get() }
1056    }
1057}
1058
1059impl PyStrategy {
1060    /// Creates a new PyStrategy instance.
1061    pub fn new(config: Option<StrategyConfig>) -> Self {
1062        let config = config.unwrap_or_default();
1063        let core = StrategyCore::new(config);
1064        let clock = PyClock::new_test();
1065        let logger = PyLogger::new(core.actor.actor_id.as_str());
1066
1067        let inner = PyStrategyInner {
1068            core,
1069            py_self: None,
1070            clock,
1071            logger,
1072        };
1073
1074        Self {
1075            inner: Rc::new(UnsafeCell::new(inner)),
1076        }
1077    }
1078
1079    /// Sets the Python instance reference for method dispatch.
1080    pub fn set_python_instance(&mut self, py_obj: Py<PyAny>) {
1081        self.inner_mut().py_self = Some(py_obj);
1082    }
1083
1084    /// Updates the runtime strategy ID.
1085    ///
1086    /// Must only be called before registration. See `PyDataActor::set_actor_id`.
1087    pub fn set_strategy_id(&mut self, strategy_id: StrategyId) -> anyhow::Result<()> {
1088        let inner = self.inner_mut();
1089        inner.core.change_id(strategy_id);
1090        Ok(())
1091    }
1092
1093    /// Updates the runtime order ID tag.
1094    pub fn set_order_id_tag(&mut self, order_id_tag: &str) -> anyhow::Result<()> {
1095        let inner = self.inner_mut();
1096        inner.core.change_order_id_tag(order_id_tag);
1097        Ok(())
1098    }
1099
1100    /// Updates the runtime log_events setting.
1101    pub fn set_log_events(&mut self, log_events: bool) {
1102        let inner = self.inner_mut();
1103        inner.core.actor.config.log_events = log_events;
1104    }
1105
1106    /// Updates the runtime log_commands setting.
1107    pub fn set_log_commands(&mut self, log_commands: bool) {
1108        let inner = self.inner_mut();
1109        inner.core.actor.config.log_commands = log_commands;
1110    }
1111
1112    /// Returns the strategy ID.
1113    pub fn strategy_id(&self) -> StrategyId {
1114        StrategyId::from(self.inner().core.actor.actor_id.inner().as_str())
1115    }
1116
1117    /// Returns a value indicating whether the strategy has been registered with a trader.
1118    pub fn is_registered(&self) -> bool {
1119        self.inner().core.actor.is_registered()
1120    }
1121
1122    /// Register the strategy with a trader.
1123    ///
1124    /// # Errors
1125    ///
1126    /// Returns an error if registration fails.
1127    pub fn register(
1128        &mut self,
1129        trader_id: TraderId,
1130        clock: Rc<RefCell<dyn Clock>>,
1131        cache: Rc<RefCell<Cache>>,
1132        portfolio: Rc<RefCell<Portfolio>>,
1133    ) -> anyhow::Result<()> {
1134        let inner = self.inner_mut();
1135        inner.core.register(trader_id, clock, cache, portfolio)?;
1136
1137        inner.clock = PyClock::from_rc(inner.core.actor.clock_rc());
1138
1139        let actor_id = inner.core.actor.actor_id.inner();
1140        let callback = TimeEventCallback::from(move |event: TimeEvent| {
1141            if let Some(mut strategy) = try_get_actor_unchecked::<PyStrategyInner>(&actor_id) {
1142                if let Err(e) = DataActor::on_time_event(&mut *strategy, &event) {
1143                    log::error!("Python time event handler failed for strategy {actor_id}: {e}");
1144                }
1145            } else {
1146                log::error!("Strategy {actor_id} not found for time event handling");
1147            }
1148        });
1149
1150        inner.clock.inner_mut().register_default_handler(callback);
1151
1152        Component::initialize(inner)
1153    }
1154
1155    /// Registers this strategy in the global component and actor registries.
1156    pub fn register_in_global_registries(&self) {
1157        let inner = self.inner();
1158        let component_id = Component::component_id(inner).inner();
1159        let actor_id = Actor::id(inner);
1160
1161        let inner_ref: Rc<UnsafeCell<PyStrategyInner>> = self.inner.clone();
1162
1163        let component_trait_ref: Rc<UnsafeCell<dyn Component>> = inner_ref.clone();
1164        with_component_registry(|registry| registry.insert(component_id, component_trait_ref));
1165
1166        let actor_trait_ref: Rc<UnsafeCell<dyn Actor>> = inner_ref;
1167        with_actor_registry(|registry| registry.insert(actor_id, actor_trait_ref));
1168    }
1169}
1170
1171#[pyo3::pymethods]
1172#[pyo3_stub_gen::derive::gen_stub_pymethods]
1173impl PyStrategy {
1174    /// Creates a new [`PyStrategy`] instance.
1175    ///
1176    /// Accepts `None` or any Python object. If the object is a [`StrategyConfig`]
1177    /// (or can be extracted as one via `from_py_object`), its values are used;
1178    /// otherwise the strategy falls back to [`StrategyConfig::default()`].
1179    ///
1180    /// This permissive signature is required so that Python subclasses can pass
1181    /// a **custom** config dataclass to their `__init__`; the Rust
1182    /// `add_strategy_from_config` then extracts `strategy_id`, `log_events`, etc.
1183    /// via `getattr` and calls the corresponding setters separately.
1184    #[new]
1185    #[pyo3(signature = (config=None))]
1186    fn py_new(config: Option<Py<PyAny>>) -> Self {
1187        let strategy_config =
1188            config.and_then(|obj| Python::attach(|py| obj.extract::<StrategyConfig>(py).ok()));
1189        Self::new(strategy_config)
1190    }
1191
1192    /// Captures the Python self reference for Rust→Python event dispatch.
1193    #[pyo3(signature = (config=None))]
1194    #[allow(unused_variables, clippy::needless_pass_by_value)]
1195    fn __init__(slf: &Bound<'_, Self>, config: Option<Py<PyAny>>) {
1196        let py_self: Py<PyAny> = slf.clone().unbind().into_any();
1197        slf.borrow_mut().set_python_instance(py_self);
1198    }
1199
1200    #[getter]
1201    #[pyo3(name = "trader_id")]
1202    fn py_trader_id(&self) -> Option<TraderId> {
1203        self.inner().core.trader_id()
1204    }
1205
1206    #[getter]
1207    #[pyo3(name = "strategy_id")]
1208    fn py_strategy_id(&self) -> StrategyId {
1209        StrategyId::from(self.inner().core.actor.actor_id.inner().as_str())
1210    }
1211
1212    #[getter]
1213    #[pyo3(name = "clock")]
1214    fn py_clock(&self) -> PyResult<PyClock> {
1215        let inner = self.inner();
1216        if inner.core.actor.is_registered() {
1217            Ok(inner.clock.clone())
1218        } else {
1219            Err(to_pyruntime_err(
1220                "Strategy must be registered with a trader before accessing clock",
1221            ))
1222        }
1223    }
1224
1225    #[getter]
1226    #[pyo3(name = "cache")]
1227    fn py_cache(&self) -> PyResult<PyCache> {
1228        let inner = self.inner();
1229        if inner.core.actor.is_registered() {
1230            Ok(PyCache::from_rc(inner.core.actor.cache_rc()))
1231        } else {
1232            Err(to_pyruntime_err(
1233                "Strategy must be registered with a trader before accessing cache",
1234            ))
1235        }
1236    }
1237
1238    #[getter]
1239    #[pyo3(name = "portfolio")]
1240    fn py_portfolio(&self) -> PyResult<PyPortfolio> {
1241        let inner = self.inner();
1242        if inner.core.actor.is_registered() {
1243            Ok(PyPortfolio::from_rc(inner.core.portfolio().clone()))
1244        } else {
1245            Err(to_pyruntime_err(
1246                "Strategy must be registered with a trader before accessing portfolio",
1247            ))
1248        }
1249    }
1250
1251    #[getter]
1252    #[pyo3(name = "log")]
1253    fn py_log(&self) -> PyLogger {
1254        self.inner().logger.clone()
1255    }
1256
1257    #[pyo3(name = "state")]
1258    fn py_state(&self) -> ComponentState {
1259        self.inner().core.actor.state()
1260    }
1261
1262    #[pyo3(name = "is_ready")]
1263    fn py_is_ready(&self) -> bool {
1264        Component::is_ready(self.inner())
1265    }
1266
1267    #[pyo3(name = "is_running")]
1268    fn py_is_running(&self) -> bool {
1269        Component::is_running(self.inner())
1270    }
1271
1272    #[pyo3(name = "is_stopped")]
1273    fn py_is_stopped(&self) -> bool {
1274        Component::is_stopped(self.inner())
1275    }
1276
1277    #[pyo3(name = "is_disposed")]
1278    fn py_is_disposed(&self) -> bool {
1279        Component::is_disposed(self.inner())
1280    }
1281
1282    #[pyo3(name = "is_degraded")]
1283    fn py_is_degraded(&self) -> bool {
1284        Component::is_degraded(self.inner())
1285    }
1286
1287    #[pyo3(name = "is_faulted")]
1288    fn py_is_faulted(&self) -> bool {
1289        Component::is_faulted(self.inner())
1290    }
1291
1292    #[pyo3(name = "start")]
1293    fn py_start(&mut self) -> PyResult<()> {
1294        Component::start(self.inner_mut()).map_err(to_pyruntime_err)
1295    }
1296
1297    #[pyo3(name = "stop")]
1298    fn py_stop(&mut self) -> PyResult<()> {
1299        Component::stop(self.inner_mut()).map_err(to_pyruntime_err)
1300    }
1301
1302    #[pyo3(name = "resume")]
1303    fn py_resume(&mut self) -> PyResult<()> {
1304        Component::resume(self.inner_mut()).map_err(to_pyruntime_err)
1305    }
1306
1307    #[pyo3(name = "reset")]
1308    fn py_reset(&mut self) -> PyResult<()> {
1309        Component::reset(self.inner_mut()).map_err(to_pyruntime_err)
1310    }
1311
1312    #[pyo3(name = "dispose")]
1313    fn py_dispose(&mut self) -> PyResult<()> {
1314        Component::dispose(self.inner_mut()).map_err(to_pyruntime_err)
1315    }
1316
1317    #[pyo3(name = "degrade")]
1318    fn py_degrade(&mut self) -> PyResult<()> {
1319        Component::degrade(self.inner_mut()).map_err(to_pyruntime_err)
1320    }
1321
1322    #[pyo3(name = "fault")]
1323    fn py_fault(&mut self) -> PyResult<()> {
1324        Component::fault(self.inner_mut()).map_err(to_pyruntime_err)
1325    }
1326
1327    #[pyo3(name = "submit_order")]
1328    #[pyo3(signature = (order, position_id=None, client_id=None, params=None))]
1329    fn py_submit_order(
1330        &mut self,
1331        py: Python<'_>,
1332        order: Py<PyAny>,
1333        position_id: Option<PositionId>,
1334        client_id: Option<ClientId>,
1335        params: Option<Py<PyDict>>,
1336    ) -> PyResult<()> {
1337        let order = pyobject_to_order_any(py, order)?;
1338        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
1339            match params {
1340                Some(dict) => from_pydict(py, &dict),
1341                None => Ok(None),
1342            }
1343        })?;
1344        let inner = self.inner_mut();
1345
1346        Strategy::submit_order(inner, order, position_id, client_id, params_map)
1347            .map_err(to_pyruntime_err)
1348    }
1349
1350    #[pyo3(name = "modify_order")]
1351    #[pyo3(signature = (client_order_id, quantity=None, price=None, trigger_price=None, client_id=None, params=None))]
1352    fn py_modify_order(
1353        &mut self,
1354        client_order_id: ClientOrderId,
1355        quantity: Option<Quantity>,
1356        price: Option<Price>,
1357        trigger_price: Option<Price>,
1358        client_id: Option<ClientId>,
1359        params: Option<Py<PyDict>>,
1360    ) -> PyResult<()> {
1361        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
1362            match params {
1363                Some(dict) => from_pydict(py, &dict),
1364                None => Ok(None),
1365            }
1366        })?;
1367        let inner = self.inner_mut();
1368
1369        Strategy::modify_order(
1370            inner,
1371            client_order_id,
1372            quantity,
1373            price,
1374            trigger_price,
1375            client_id,
1376            params_map,
1377        )
1378        .map_err(to_pyruntime_err)
1379    }
1380
1381    #[pyo3(name = "cancel_order")]
1382    #[pyo3(signature = (client_order_id, client_id=None, params=None))]
1383    fn py_cancel_order(
1384        &mut self,
1385        client_order_id: ClientOrderId,
1386        client_id: Option<ClientId>,
1387        params: Option<Py<PyDict>>,
1388    ) -> PyResult<()> {
1389        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
1390            match params {
1391                Some(dict) => from_pydict(py, &dict),
1392                None => Ok(None),
1393            }
1394        })?;
1395        let inner = self.inner_mut();
1396
1397        Strategy::cancel_order(inner, client_order_id, client_id, params_map)
1398            .map_err(to_pyruntime_err)
1399    }
1400
1401    #[pyo3(name = "cancel_orders")]
1402    #[pyo3(signature = (client_order_ids, client_id=None, params=None))]
1403    fn py_cancel_orders(
1404        &mut self,
1405        client_order_ids: Vec<ClientOrderId>,
1406        client_id: Option<ClientId>,
1407        params: Option<Py<PyDict>>,
1408    ) -> PyResult<()> {
1409        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
1410            match params {
1411                Some(dict) => from_pydict(py, &dict),
1412                None => Ok(None),
1413            }
1414        })?;
1415
1416        Strategy::cancel_orders(self.inner_mut(), client_order_ids, client_id, params_map)
1417            .map_err(to_pyruntime_err)
1418    }
1419
1420    #[pyo3(name = "cancel_all_orders")]
1421    #[pyo3(signature = (instrument_id, order_side=None, client_id=None, params=None))]
1422    fn py_cancel_all_orders(
1423        &mut self,
1424        instrument_id: InstrumentId,
1425        order_side: Option<OrderSide>,
1426        client_id: Option<ClientId>,
1427        params: Option<Py<PyDict>>,
1428    ) -> PyResult<()> {
1429        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
1430            match params {
1431                Some(dict) => from_pydict(py, &dict),
1432                None => Ok(None),
1433            }
1434        })?;
1435        Strategy::cancel_all_orders(
1436            self.inner_mut(),
1437            instrument_id,
1438            order_side,
1439            client_id,
1440            params_map,
1441        )
1442        .map_err(to_pyruntime_err)
1443    }
1444
1445    #[pyo3(name = "close_position")]
1446    #[pyo3(signature = (position, client_id=None, tags=None, time_in_force=None, reduce_only=None, quote_quantity=None))]
1447    fn py_close_position(
1448        &mut self,
1449        position: &Position,
1450        client_id: Option<ClientId>,
1451        tags: Option<Vec<String>>,
1452        time_in_force: Option<TimeInForce>,
1453        reduce_only: Option<bool>,
1454        quote_quantity: Option<bool>,
1455    ) -> PyResult<()> {
1456        let tags = tags.map(|t| t.into_iter().map(|s| Ustr::from(&s)).collect());
1457        Strategy::close_position(
1458            self.inner_mut(),
1459            position,
1460            client_id,
1461            tags,
1462            time_in_force,
1463            reduce_only,
1464            quote_quantity,
1465        )
1466        .map_err(to_pyruntime_err)
1467    }
1468
1469    #[pyo3(name = "close_all_positions")]
1470    #[pyo3(signature = (instrument_id, position_side=None, client_id=None, tags=None, time_in_force=None, reduce_only=None, quote_quantity=None))]
1471    #[expect(clippy::too_many_arguments)]
1472    fn py_close_all_positions(
1473        &mut self,
1474        instrument_id: InstrumentId,
1475        position_side: Option<PositionSide>,
1476        client_id: Option<ClientId>,
1477        tags: Option<Vec<String>>,
1478        time_in_force: Option<TimeInForce>,
1479        reduce_only: Option<bool>,
1480        quote_quantity: Option<bool>,
1481    ) -> PyResult<()> {
1482        let tags = tags.map(|t| t.into_iter().map(|s| Ustr::from(&s)).collect());
1483        Strategy::close_all_positions(
1484            self.inner_mut(),
1485            instrument_id,
1486            position_side,
1487            client_id,
1488            tags,
1489            time_in_force,
1490            reduce_only,
1491            quote_quantity,
1492        )
1493        .map_err(to_pyruntime_err)
1494    }
1495
1496    #[pyo3(name = "query_account")]
1497    #[pyo3(signature = (account_id, client_id=None, params=None))]
1498    fn py_query_account(
1499        &mut self,
1500        py: Python<'_>,
1501        account_id: AccountId,
1502        client_id: Option<ClientId>,
1503        params: Option<Py<PyDict>>,
1504    ) -> PyResult<()> {
1505        let params_map = match params {
1506            Some(dict) => from_pydict(py, &dict)?,
1507            None => None,
1508        };
1509        Strategy::query_account(self.inner_mut(), account_id, client_id, params_map)
1510            .map_err(to_pyruntime_err)
1511    }
1512
1513    #[pyo3(name = "query_order")]
1514    #[pyo3(signature = (order, client_id=None, params=None))]
1515    fn py_query_order(
1516        &mut self,
1517        py: Python<'_>,
1518        order: Py<PyAny>,
1519        client_id: Option<ClientId>,
1520        params: Option<Py<PyDict>>,
1521    ) -> PyResult<()> {
1522        let order = pyobject_to_order_any(py, order)?;
1523        let params_map = match params {
1524            Some(dict) => from_pydict(py, &dict)?,
1525            None => None,
1526        };
1527        Strategy::query_order(self.inner_mut(), &order, client_id, params_map)
1528            .map_err(to_pyruntime_err)
1529    }
1530
1531    #[pyo3(name = "on_start")]
1532    fn py_on_start(&mut self) {}
1533
1534    #[pyo3(name = "on_stop")]
1535    fn py_on_stop(&mut self) {}
1536
1537    #[pyo3(name = "on_resume")]
1538    fn py_on_resume(&mut self) {}
1539
1540    #[pyo3(name = "on_reset")]
1541    fn py_on_reset(&mut self) {}
1542
1543    #[pyo3(name = "on_dispose")]
1544    fn py_on_dispose(&mut self) {}
1545
1546    #[pyo3(name = "on_degrade")]
1547    fn py_on_degrade(&mut self) {}
1548
1549    #[pyo3(name = "on_fault")]
1550    fn py_on_fault(&mut self) {}
1551
1552    #[allow(unused_variables, clippy::needless_pass_by_value)]
1553    #[pyo3(name = "on_time_event")]
1554    fn py_on_time_event(&mut self, event: TimeEvent) {}
1555
1556    #[allow(unused_variables, clippy::needless_pass_by_value)]
1557    #[pyo3(name = "on_data")]
1558    fn py_on_data(&mut self, data: Py<PyAny>) {}
1559
1560    #[allow(unused_variables)]
1561    #[pyo3(name = "on_signal")]
1562    fn py_on_signal(&mut self, signal: &Signal) {}
1563
1564    #[allow(unused_variables, clippy::needless_pass_by_value)]
1565    #[pyo3(name = "on_instrument")]
1566    fn py_on_instrument(&mut self, instrument: Py<PyAny>) {}
1567
1568    #[allow(unused_variables)]
1569    #[pyo3(name = "on_quote")]
1570    fn py_on_quote(&mut self, quote: QuoteTick) {}
1571
1572    #[allow(unused_variables)]
1573    #[pyo3(name = "on_trade")]
1574    fn py_on_trade(&mut self, trade: TradeTick) {}
1575
1576    #[allow(unused_variables)]
1577    #[pyo3(name = "on_bar")]
1578    fn py_on_bar(&mut self, bar: Bar) {}
1579
1580    #[allow(unused_variables, clippy::needless_pass_by_value)]
1581    #[pyo3(name = "on_book_deltas")]
1582    fn py_on_book_deltas(&mut self, deltas: OrderBookDeltas) {}
1583
1584    #[allow(unused_variables)]
1585    #[pyo3(name = "on_book")]
1586    fn py_on_book(&mut self, book: &OrderBook) {}
1587
1588    #[allow(unused_variables)]
1589    #[pyo3(name = "on_mark_price")]
1590    fn py_on_mark_price(&mut self, mark_price: MarkPriceUpdate) {}
1591
1592    #[allow(unused_variables)]
1593    #[pyo3(name = "on_index_price")]
1594    fn py_on_index_price(&mut self, index_price: IndexPriceUpdate) {}
1595
1596    #[allow(unused_variables)]
1597    #[pyo3(name = "on_funding_rate")]
1598    fn py_on_funding_rate(&mut self, funding_rate: FundingRateUpdate) {}
1599
1600    #[allow(unused_variables)]
1601    #[pyo3(name = "on_instrument_status")]
1602    fn py_on_instrument_status(&mut self, status: InstrumentStatus) {}
1603
1604    #[allow(unused_variables)]
1605    #[pyo3(name = "on_instrument_close")]
1606    fn py_on_instrument_close(&mut self, close: InstrumentClose) {}
1607
1608    #[allow(unused_variables)]
1609    #[pyo3(name = "on_option_greeks")]
1610    fn py_on_option_greeks(&mut self, greeks: OptionGreeks) {}
1611
1612    #[allow(unused_variables, clippy::needless_pass_by_value)]
1613    #[pyo3(name = "on_option_chain")]
1614    fn py_on_option_chain(&mut self, slice: OptionChainSlice) {}
1615
1616    #[allow(unused_variables, clippy::needless_pass_by_value)]
1617    #[pyo3(name = "on_order_initialized")]
1618    fn py_on_order_initialized(&mut self, event: OrderInitialized) {}
1619
1620    #[allow(unused_variables)]
1621    #[pyo3(name = "on_order_denied")]
1622    fn py_on_order_denied(&mut self, event: OrderDenied) {}
1623
1624    #[allow(unused_variables)]
1625    #[pyo3(name = "on_order_emulated")]
1626    fn py_on_order_emulated(&mut self, event: OrderEmulated) {}
1627
1628    #[allow(unused_variables)]
1629    #[pyo3(name = "on_order_released")]
1630    fn py_on_order_released(&mut self, event: OrderReleased) {}
1631
1632    #[allow(unused_variables)]
1633    #[pyo3(name = "on_order_submitted")]
1634    fn py_on_order_submitted(&mut self, event: OrderSubmitted) {}
1635
1636    #[allow(unused_variables)]
1637    #[pyo3(name = "on_order_rejected")]
1638    fn py_on_order_rejected(&mut self, event: OrderRejected) {}
1639
1640    #[allow(unused_variables)]
1641    #[pyo3(name = "on_order_accepted")]
1642    fn py_on_order_accepted(&mut self, event: OrderAccepted) {}
1643
1644    #[allow(unused_variables)]
1645    #[pyo3(name = "on_order_expired")]
1646    fn py_on_order_expired(&mut self, event: OrderExpired) {}
1647
1648    #[allow(unused_variables)]
1649    #[pyo3(name = "on_order_triggered")]
1650    fn py_on_order_triggered(&mut self, event: OrderTriggered) {}
1651
1652    #[allow(unused_variables)]
1653    #[pyo3(name = "on_order_pending_update")]
1654    fn py_on_order_pending_update(&mut self, event: OrderPendingUpdate) {}
1655
1656    #[allow(unused_variables)]
1657    #[pyo3(name = "on_order_pending_cancel")]
1658    fn py_on_order_pending_cancel(&mut self, event: OrderPendingCancel) {}
1659
1660    #[allow(unused_variables)]
1661    #[pyo3(name = "on_order_modify_rejected")]
1662    fn py_on_order_modify_rejected(&mut self, event: OrderModifyRejected) {}
1663
1664    #[allow(unused_variables)]
1665    #[pyo3(name = "on_order_cancel_rejected")]
1666    fn py_on_order_cancel_rejected(&mut self, event: OrderCancelRejected) {}
1667
1668    #[allow(unused_variables)]
1669    #[pyo3(name = "on_order_updated")]
1670    fn py_on_order_updated(&mut self, event: OrderUpdated) {}
1671
1672    #[allow(unused_variables)]
1673    #[pyo3(name = "on_order_canceled")]
1674    fn py_on_order_canceled(&mut self, event: OrderCanceled) {}
1675
1676    #[allow(unused_variables)]
1677    #[pyo3(name = "on_order_filled")]
1678    fn py_on_order_filled(&mut self, event: OrderFilled) {}
1679
1680    #[allow(unused_variables, clippy::needless_pass_by_value)]
1681    #[pyo3(name = "on_position_opened")]
1682    fn py_on_position_opened(&mut self, event: PositionOpened) {}
1683
1684    #[allow(unused_variables, clippy::needless_pass_by_value)]
1685    #[pyo3(name = "on_position_changed")]
1686    fn py_on_position_changed(&mut self, event: PositionChanged) {}
1687
1688    #[allow(unused_variables, clippy::needless_pass_by_value)]
1689    #[pyo3(name = "on_position_closed")]
1690    fn py_on_position_closed(&mut self, event: PositionClosed) {}
1691
1692    #[allow(unused_variables, clippy::needless_pass_by_value)]
1693    #[pyo3(name = "on_historical_data")]
1694    fn py_on_historical_data(&mut self, data: Py<PyAny>) {
1695        // Default implementation - can be overridden in Python subclasses
1696    }
1697
1698    #[allow(unused_variables, clippy::needless_pass_by_value)]
1699    #[pyo3(name = "on_historical_quotes")]
1700    fn py_on_historical_quotes(&mut self, quotes: Vec<QuoteTick>) {
1701        // Default implementation - can be overridden in Python subclasses
1702    }
1703
1704    #[allow(unused_variables, clippy::needless_pass_by_value)]
1705    #[pyo3(name = "on_historical_trades")]
1706    fn py_on_historical_trades(&mut self, trades: Vec<TradeTick>) {
1707        // Default implementation - can be overridden in Python subclasses
1708    }
1709
1710    #[allow(unused_variables, clippy::needless_pass_by_value)]
1711    #[pyo3(name = "on_historical_funding_rates")]
1712    fn py_on_historical_funding_rates(&mut self, funding_rates: Vec<FundingRateUpdate>) {
1713        // Default implementation - can be overridden in Python subclasses
1714    }
1715
1716    #[allow(unused_variables, clippy::needless_pass_by_value)]
1717    #[pyo3(name = "on_historical_bars")]
1718    fn py_on_historical_bars(&mut self, bars: Vec<Bar>) {
1719        // Default implementation - can be overridden in Python subclasses
1720    }
1721
1722    #[allow(unused_variables, clippy::needless_pass_by_value)]
1723    #[pyo3(name = "on_historical_mark_prices")]
1724    fn py_on_historical_mark_prices(&mut self, mark_prices: Vec<MarkPriceUpdate>) {
1725        // Default implementation - can be overridden in Python subclasses
1726    }
1727
1728    #[allow(unused_variables, clippy::needless_pass_by_value)]
1729    #[pyo3(name = "on_historical_index_prices")]
1730    fn py_on_historical_index_prices(&mut self, index_prices: Vec<IndexPriceUpdate>) {
1731        // Default implementation - can be overridden in Python subclasses
1732    }
1733
1734    #[pyo3(name = "subscribe_data")]
1735    #[pyo3(signature = (data_type, client_id=None, params=None))]
1736    fn py_subscribe_data(
1737        &mut self,
1738        data_type: DataType,
1739        client_id: Option<ClientId>,
1740        params: Option<Py<PyDict>>,
1741    ) -> PyResult<()> {
1742        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
1743            match params {
1744                Some(dict) => from_pydict(py, &dict),
1745                None => Ok(None),
1746            }
1747        })?;
1748        DataActor::subscribe_data(self.inner_mut(), data_type, client_id, params_map);
1749        Ok(())
1750    }
1751
1752    #[pyo3(name = "subscribe_instruments")]
1753    #[pyo3(signature = (venue, client_id=None, params=None))]
1754    fn py_subscribe_instruments(
1755        &mut self,
1756        venue: Venue,
1757        client_id: Option<ClientId>,
1758        params: Option<Py<PyDict>>,
1759    ) -> PyResult<()> {
1760        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
1761            match params {
1762                Some(dict) => from_pydict(py, &dict),
1763                None => Ok(None),
1764            }
1765        })?;
1766        DataActor::subscribe_instruments(self.inner_mut(), venue, client_id, params_map);
1767        Ok(())
1768    }
1769
1770    #[pyo3(name = "subscribe_instrument")]
1771    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1772    fn py_subscribe_instrument(
1773        &mut self,
1774        instrument_id: InstrumentId,
1775        client_id: Option<ClientId>,
1776        params: Option<Py<PyDict>>,
1777    ) -> PyResult<()> {
1778        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
1779            match params {
1780                Some(dict) => from_pydict(py, &dict),
1781                None => Ok(None),
1782            }
1783        })?;
1784        DataActor::subscribe_instrument(self.inner_mut(), instrument_id, client_id, params_map);
1785        Ok(())
1786    }
1787
1788    #[pyo3(name = "subscribe_book_deltas")]
1789    #[pyo3(signature = (instrument_id, book_type, depth=None, client_id=None, managed=false, params=None))]
1790    fn py_subscribe_book_deltas(
1791        &mut self,
1792        instrument_id: InstrumentId,
1793        book_type: BookType,
1794        depth: Option<usize>,
1795        client_id: Option<ClientId>,
1796        managed: bool,
1797        params: Option<Py<PyDict>>,
1798    ) -> PyResult<()> {
1799        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
1800            match params {
1801                Some(dict) => from_pydict(py, &dict),
1802                None => Ok(None),
1803            }
1804        })?;
1805        let depth = depth.and_then(NonZeroUsize::new);
1806        DataActor::subscribe_book_deltas(
1807            self.inner_mut(),
1808            instrument_id,
1809            book_type,
1810            depth,
1811            client_id,
1812            managed,
1813            params_map,
1814        );
1815        Ok(())
1816    }
1817
1818    #[pyo3(name = "subscribe_book_at_interval")]
1819    #[pyo3(signature = (instrument_id, book_type, interval_ms, depth=None, client_id=None, params=None))]
1820    fn py_subscribe_book_at_interval(
1821        &mut self,
1822        instrument_id: InstrumentId,
1823        book_type: BookType,
1824        interval_ms: usize,
1825        depth: Option<usize>,
1826        client_id: Option<ClientId>,
1827        params: Option<Py<PyDict>>,
1828    ) -> PyResult<()> {
1829        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
1830            match params {
1831                Some(dict) => from_pydict(py, &dict),
1832                None => Ok(None),
1833            }
1834        })?;
1835        let depth = depth.and_then(NonZeroUsize::new);
1836        let interval_ms = NonZeroUsize::new(interval_ms)
1837            .ok_or_else(|| to_pyvalue_err("interval_ms must be > 0"))?;
1838
1839        DataActor::subscribe_book_at_interval(
1840            self.inner_mut(),
1841            instrument_id,
1842            book_type,
1843            depth,
1844            interval_ms,
1845            client_id,
1846            params_map,
1847        );
1848        Ok(())
1849    }
1850
1851    #[pyo3(name = "subscribe_quotes")]
1852    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1853    fn py_subscribe_quotes(
1854        &mut self,
1855        instrument_id: InstrumentId,
1856        client_id: Option<ClientId>,
1857        params: Option<Py<PyDict>>,
1858    ) -> PyResult<()> {
1859        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
1860            match params {
1861                Some(dict) => from_pydict(py, &dict),
1862                None => Ok(None),
1863            }
1864        })?;
1865        DataActor::subscribe_quotes(self.inner_mut(), instrument_id, client_id, params_map);
1866        Ok(())
1867    }
1868
1869    #[pyo3(name = "subscribe_trades")]
1870    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1871    fn py_subscribe_trades(
1872        &mut self,
1873        instrument_id: InstrumentId,
1874        client_id: Option<ClientId>,
1875        params: Option<Py<PyDict>>,
1876    ) -> PyResult<()> {
1877        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
1878            match params {
1879                Some(dict) => from_pydict(py, &dict),
1880                None => Ok(None),
1881            }
1882        })?;
1883        DataActor::subscribe_trades(self.inner_mut(), instrument_id, client_id, params_map);
1884        Ok(())
1885    }
1886
1887    #[pyo3(name = "subscribe_bars")]
1888    #[pyo3(signature = (bar_type, client_id=None, params=None))]
1889    fn py_subscribe_bars(
1890        &mut self,
1891        bar_type: BarType,
1892        client_id: Option<ClientId>,
1893        params: Option<Py<PyDict>>,
1894    ) -> PyResult<()> {
1895        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
1896            match params {
1897                Some(dict) => from_pydict(py, &dict),
1898                None => Ok(None),
1899            }
1900        })?;
1901        DataActor::subscribe_bars(self.inner_mut(), bar_type, client_id, params_map);
1902        Ok(())
1903    }
1904
1905    #[pyo3(name = "subscribe_mark_prices")]
1906    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1907    fn py_subscribe_mark_prices(
1908        &mut self,
1909        instrument_id: InstrumentId,
1910        client_id: Option<ClientId>,
1911        params: Option<Py<PyDict>>,
1912    ) -> PyResult<()> {
1913        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
1914            match params {
1915                Some(dict) => from_pydict(py, &dict),
1916                None => Ok(None),
1917            }
1918        })?;
1919        DataActor::subscribe_mark_prices(self.inner_mut(), instrument_id, client_id, params_map);
1920        Ok(())
1921    }
1922
1923    #[pyo3(name = "subscribe_index_prices")]
1924    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1925    fn py_subscribe_index_prices(
1926        &mut self,
1927        instrument_id: InstrumentId,
1928        client_id: Option<ClientId>,
1929        params: Option<Py<PyDict>>,
1930    ) -> PyResult<()> {
1931        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
1932            match params {
1933                Some(dict) => from_pydict(py, &dict),
1934                None => Ok(None),
1935            }
1936        })?;
1937        DataActor::subscribe_index_prices(self.inner_mut(), instrument_id, client_id, params_map);
1938        Ok(())
1939    }
1940
1941    #[pyo3(name = "subscribe_funding_rates")]
1942    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1943    fn py_subscribe_funding_rates(
1944        &mut self,
1945        instrument_id: InstrumentId,
1946        client_id: Option<ClientId>,
1947        params: Option<Py<PyDict>>,
1948    ) -> PyResult<()> {
1949        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
1950            match params {
1951                Some(dict) => from_pydict(py, &dict),
1952                None => Ok(None),
1953            }
1954        })?;
1955        DataActor::subscribe_funding_rates(self.inner_mut(), instrument_id, client_id, params_map);
1956        Ok(())
1957    }
1958
1959    #[pyo3(name = "subscribe_option_greeks")]
1960    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1961    fn py_subscribe_option_greeks(
1962        &mut self,
1963        instrument_id: InstrumentId,
1964        client_id: Option<ClientId>,
1965        params: Option<Py<PyDict>>,
1966    ) -> PyResult<()> {
1967        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
1968            match params {
1969                Some(dict) => from_pydict(py, &dict),
1970                None => Ok(None),
1971            }
1972        })?;
1973        DataActor::subscribe_option_greeks(self.inner_mut(), instrument_id, client_id, params_map);
1974        Ok(())
1975    }
1976
1977    #[pyo3(name = "subscribe_instrument_status")]
1978    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1979    fn py_subscribe_instrument_status(
1980        &mut self,
1981        instrument_id: InstrumentId,
1982        client_id: Option<ClientId>,
1983        params: Option<Py<PyDict>>,
1984    ) -> PyResult<()> {
1985        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
1986            match params {
1987                Some(dict) => from_pydict(py, &dict),
1988                None => Ok(None),
1989            }
1990        })?;
1991        DataActor::subscribe_instrument_status(
1992            self.inner_mut(),
1993            instrument_id,
1994            client_id,
1995            params_map,
1996        );
1997        Ok(())
1998    }
1999
2000    #[pyo3(name = "subscribe_instrument_close")]
2001    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2002    fn py_subscribe_instrument_close(
2003        &mut self,
2004        instrument_id: InstrumentId,
2005        client_id: Option<ClientId>,
2006        params: Option<Py<PyDict>>,
2007    ) -> PyResult<()> {
2008        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2009            match params {
2010                Some(dict) => from_pydict(py, &dict),
2011                None => Ok(None),
2012            }
2013        })?;
2014        DataActor::subscribe_instrument_close(
2015            self.inner_mut(),
2016            instrument_id,
2017            client_id,
2018            params_map,
2019        );
2020        Ok(())
2021    }
2022
2023    #[pyo3(name = "subscribe_option_chain")]
2024    #[pyo3(signature = (series_id, strike_range, snapshot_interval_ms=None, client_id=None, params=None))]
2025    fn py_subscribe_option_chain(
2026        &mut self,
2027        py: Python<'_>,
2028        series_id: OptionSeriesId,
2029        strike_range: PyStrikeRange,
2030        snapshot_interval_ms: Option<u64>,
2031        client_id: Option<ClientId>,
2032        params: Option<Py<PyDict>>,
2033    ) -> PyResult<()> {
2034        let params_map = match params {
2035            Some(dict) => from_pydict(py, &dict)?,
2036            None => None,
2037        };
2038        DataActor::subscribe_option_chain(
2039            self.inner_mut(),
2040            series_id,
2041            strike_range.inner,
2042            snapshot_interval_ms,
2043            client_id,
2044            params_map,
2045        );
2046        Ok(())
2047    }
2048
2049    #[pyo3(name = "subscribe_order_fills")]
2050    #[pyo3(signature = (instrument_id))]
2051    fn py_subscribe_order_fills(&mut self, instrument_id: InstrumentId) {
2052        DataActor::subscribe_order_fills(self.inner_mut(), instrument_id);
2053    }
2054
2055    #[pyo3(name = "subscribe_order_cancels")]
2056    #[pyo3(signature = (instrument_id))]
2057    fn py_subscribe_order_cancels(&mut self, instrument_id: InstrumentId) {
2058        DataActor::subscribe_order_cancels(self.inner_mut(), instrument_id);
2059    }
2060
2061    #[pyo3(name = "unsubscribe_data")]
2062    #[pyo3(signature = (data_type, client_id=None, params=None))]
2063    fn py_unsubscribe_data(
2064        &mut self,
2065        data_type: DataType,
2066        client_id: Option<ClientId>,
2067        params: Option<Py<PyDict>>,
2068    ) -> PyResult<()> {
2069        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2070            match params {
2071                Some(dict) => from_pydict(py, &dict),
2072                None => Ok(None),
2073            }
2074        })?;
2075        DataActor::unsubscribe_data(self.inner_mut(), data_type, client_id, params_map);
2076        Ok(())
2077    }
2078
2079    #[pyo3(name = "unsubscribe_instruments")]
2080    #[pyo3(signature = (venue, client_id=None, params=None))]
2081    fn py_unsubscribe_instruments(
2082        &mut self,
2083        venue: Venue,
2084        client_id: Option<ClientId>,
2085        params: Option<Py<PyDict>>,
2086    ) -> PyResult<()> {
2087        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2088            match params {
2089                Some(dict) => from_pydict(py, &dict),
2090                None => Ok(None),
2091            }
2092        })?;
2093        DataActor::unsubscribe_instruments(self.inner_mut(), venue, client_id, params_map);
2094        Ok(())
2095    }
2096
2097    #[pyo3(name = "unsubscribe_instrument")]
2098    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2099    fn py_unsubscribe_instrument(
2100        &mut self,
2101        instrument_id: InstrumentId,
2102        client_id: Option<ClientId>,
2103        params: Option<Py<PyDict>>,
2104    ) -> PyResult<()> {
2105        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2106            match params {
2107                Some(dict) => from_pydict(py, &dict),
2108                None => Ok(None),
2109            }
2110        })?;
2111        DataActor::unsubscribe_instrument(self.inner_mut(), instrument_id, client_id, params_map);
2112        Ok(())
2113    }
2114
2115    #[pyo3(name = "unsubscribe_book_deltas")]
2116    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2117    fn py_unsubscribe_book_deltas(
2118        &mut self,
2119        instrument_id: InstrumentId,
2120        client_id: Option<ClientId>,
2121        params: Option<Py<PyDict>>,
2122    ) -> PyResult<()> {
2123        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2124            match params {
2125                Some(dict) => from_pydict(py, &dict),
2126                None => Ok(None),
2127            }
2128        })?;
2129        DataActor::unsubscribe_book_deltas(self.inner_mut(), instrument_id, client_id, params_map);
2130        Ok(())
2131    }
2132
2133    #[pyo3(name = "unsubscribe_book_at_interval")]
2134    #[pyo3(signature = (instrument_id, interval_ms, client_id=None, params=None))]
2135    fn py_unsubscribe_book_at_interval(
2136        &mut self,
2137        instrument_id: InstrumentId,
2138        interval_ms: usize,
2139        client_id: Option<ClientId>,
2140        params: Option<Py<PyDict>>,
2141    ) -> PyResult<()> {
2142        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2143            match params {
2144                Some(dict) => from_pydict(py, &dict),
2145                None => Ok(None),
2146            }
2147        })?;
2148        let interval_ms = NonZeroUsize::new(interval_ms)
2149            .ok_or_else(|| to_pyvalue_err("interval_ms must be > 0"))?;
2150
2151        DataActor::unsubscribe_book_at_interval(
2152            self.inner_mut(),
2153            instrument_id,
2154            interval_ms,
2155            client_id,
2156            params_map,
2157        );
2158        Ok(())
2159    }
2160
2161    #[pyo3(name = "unsubscribe_quotes")]
2162    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2163    fn py_unsubscribe_quotes(
2164        &mut self,
2165        instrument_id: InstrumentId,
2166        client_id: Option<ClientId>,
2167        params: Option<Py<PyDict>>,
2168    ) -> PyResult<()> {
2169        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2170            match params {
2171                Some(dict) => from_pydict(py, &dict),
2172                None => Ok(None),
2173            }
2174        })?;
2175        DataActor::unsubscribe_quotes(self.inner_mut(), instrument_id, client_id, params_map);
2176        Ok(())
2177    }
2178
2179    #[pyo3(name = "unsubscribe_trades")]
2180    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2181    fn py_unsubscribe_trades(
2182        &mut self,
2183        instrument_id: InstrumentId,
2184        client_id: Option<ClientId>,
2185        params: Option<Py<PyDict>>,
2186    ) -> PyResult<()> {
2187        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2188            match params {
2189                Some(dict) => from_pydict(py, &dict),
2190                None => Ok(None),
2191            }
2192        })?;
2193        DataActor::unsubscribe_trades(self.inner_mut(), instrument_id, client_id, params_map);
2194        Ok(())
2195    }
2196
2197    #[pyo3(name = "unsubscribe_bars")]
2198    #[pyo3(signature = (bar_type, client_id=None, params=None))]
2199    fn py_unsubscribe_bars(
2200        &mut self,
2201        bar_type: BarType,
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        DataActor::unsubscribe_bars(self.inner_mut(), bar_type, client_id, params_map);
2212        Ok(())
2213    }
2214
2215    #[pyo3(name = "unsubscribe_mark_prices")]
2216    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2217    fn py_unsubscribe_mark_prices(
2218        &mut self,
2219        instrument_id: InstrumentId,
2220        client_id: Option<ClientId>,
2221        params: Option<Py<PyDict>>,
2222    ) -> PyResult<()> {
2223        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2224            match params {
2225                Some(dict) => from_pydict(py, &dict),
2226                None => Ok(None),
2227            }
2228        })?;
2229        DataActor::unsubscribe_mark_prices(self.inner_mut(), instrument_id, client_id, params_map);
2230        Ok(())
2231    }
2232
2233    #[pyo3(name = "unsubscribe_index_prices")]
2234    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2235    fn py_unsubscribe_index_prices(
2236        &mut self,
2237        instrument_id: InstrumentId,
2238        client_id: Option<ClientId>,
2239        params: Option<Py<PyDict>>,
2240    ) -> PyResult<()> {
2241        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2242            match params {
2243                Some(dict) => from_pydict(py, &dict),
2244                None => Ok(None),
2245            }
2246        })?;
2247        DataActor::unsubscribe_index_prices(self.inner_mut(), instrument_id, client_id, params_map);
2248        Ok(())
2249    }
2250
2251    #[pyo3(name = "unsubscribe_funding_rates")]
2252    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2253    fn py_unsubscribe_funding_rates(
2254        &mut self,
2255        instrument_id: InstrumentId,
2256        client_id: Option<ClientId>,
2257        params: Option<Py<PyDict>>,
2258    ) -> PyResult<()> {
2259        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2260            match params {
2261                Some(dict) => from_pydict(py, &dict),
2262                None => Ok(None),
2263            }
2264        })?;
2265        DataActor::unsubscribe_funding_rates(
2266            self.inner_mut(),
2267            instrument_id,
2268            client_id,
2269            params_map,
2270        );
2271        Ok(())
2272    }
2273
2274    #[pyo3(name = "unsubscribe_option_greeks")]
2275    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2276    fn py_unsubscribe_option_greeks(
2277        &mut self,
2278        instrument_id: InstrumentId,
2279        client_id: Option<ClientId>,
2280        params: Option<Py<PyDict>>,
2281    ) -> PyResult<()> {
2282        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2283            match params {
2284                Some(dict) => from_pydict(py, &dict),
2285                None => Ok(None),
2286            }
2287        })?;
2288        DataActor::unsubscribe_option_greeks(
2289            self.inner_mut(),
2290            instrument_id,
2291            client_id,
2292            params_map,
2293        );
2294        Ok(())
2295    }
2296
2297    #[pyo3(name = "unsubscribe_instrument_status")]
2298    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2299    fn py_unsubscribe_instrument_status(
2300        &mut self,
2301        instrument_id: InstrumentId,
2302        client_id: Option<ClientId>,
2303        params: Option<Py<PyDict>>,
2304    ) -> PyResult<()> {
2305        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2306            match params {
2307                Some(dict) => from_pydict(py, &dict),
2308                None => Ok(None),
2309            }
2310        })?;
2311        DataActor::unsubscribe_instrument_status(
2312            self.inner_mut(),
2313            instrument_id,
2314            client_id,
2315            params_map,
2316        );
2317        Ok(())
2318    }
2319
2320    #[pyo3(name = "unsubscribe_instrument_close")]
2321    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2322    fn py_unsubscribe_instrument_close(
2323        &mut self,
2324        instrument_id: InstrumentId,
2325        client_id: Option<ClientId>,
2326        params: Option<Py<PyDict>>,
2327    ) -> PyResult<()> {
2328        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2329            match params {
2330                Some(dict) => from_pydict(py, &dict),
2331                None => Ok(None),
2332            }
2333        })?;
2334        DataActor::unsubscribe_instrument_close(
2335            self.inner_mut(),
2336            instrument_id,
2337            client_id,
2338            params_map,
2339        );
2340        Ok(())
2341    }
2342
2343    #[pyo3(name = "unsubscribe_option_chain")]
2344    #[pyo3(signature = (series_id, client_id=None))]
2345    fn py_unsubscribe_option_chain(
2346        &mut self,
2347        series_id: OptionSeriesId,
2348        client_id: Option<ClientId>,
2349    ) {
2350        DataActor::unsubscribe_option_chain(self.inner_mut(), series_id, client_id);
2351    }
2352
2353    #[pyo3(name = "unsubscribe_order_fills")]
2354    #[pyo3(signature = (instrument_id))]
2355    fn py_unsubscribe_order_fills(&mut self, instrument_id: InstrumentId) {
2356        DataActor::unsubscribe_order_fills(self.inner_mut(), instrument_id);
2357    }
2358
2359    #[pyo3(name = "unsubscribe_order_cancels")]
2360    #[pyo3(signature = (instrument_id))]
2361    fn py_unsubscribe_order_cancels(&mut self, instrument_id: InstrumentId) {
2362        DataActor::unsubscribe_order_cancels(self.inner_mut(), instrument_id);
2363    }
2364
2365    #[pyo3(name = "request_data")]
2366    #[pyo3(signature = (data_type, client_id, start=None, end=None, limit=None, params=None))]
2367    fn py_request_data(
2368        &mut self,
2369        data_type: DataType,
2370        client_id: ClientId,
2371        start: Option<u64>,
2372        end: Option<u64>,
2373        limit: Option<usize>,
2374        params: Option<Py<PyDict>>,
2375    ) -> PyResult<String> {
2376        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2377            match params {
2378                Some(dict) => from_pydict(py, &dict),
2379                None => Ok(None),
2380            }
2381        })?;
2382        let limit = limit.and_then(NonZeroUsize::new);
2383        let start = start.map(|ts| UnixNanos::from(ts).to_datetime_utc());
2384        let end = end.map(|ts| UnixNanos::from(ts).to_datetime_utc());
2385
2386        let request_id = DataActor::request_data(
2387            self.inner_mut(),
2388            data_type,
2389            client_id,
2390            start,
2391            end,
2392            limit,
2393            params_map,
2394        )
2395        .map_err(to_pyvalue_err)?;
2396        Ok(request_id.to_string())
2397    }
2398
2399    #[pyo3(name = "request_instrument")]
2400    #[pyo3(signature = (instrument_id, start=None, end=None, client_id=None, params=None))]
2401    fn py_request_instrument(
2402        &mut self,
2403        instrument_id: InstrumentId,
2404        start: Option<u64>,
2405        end: Option<u64>,
2406        client_id: Option<ClientId>,
2407        params: Option<Py<PyDict>>,
2408    ) -> PyResult<String> {
2409        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2410            match params {
2411                Some(dict) => from_pydict(py, &dict),
2412                None => Ok(None),
2413            }
2414        })?;
2415        let start = start.map(|ts| UnixNanos::from(ts).to_datetime_utc());
2416        let end = end.map(|ts| UnixNanos::from(ts).to_datetime_utc());
2417
2418        let request_id = DataActor::request_instrument(
2419            self.inner_mut(),
2420            instrument_id,
2421            start,
2422            end,
2423            client_id,
2424            params_map,
2425        )
2426        .map_err(to_pyvalue_err)?;
2427        Ok(request_id.to_string())
2428    }
2429
2430    #[pyo3(name = "request_instruments")]
2431    #[pyo3(signature = (venue=None, start=None, end=None, client_id=None, params=None))]
2432    fn py_request_instruments(
2433        &mut self,
2434        venue: Option<Venue>,
2435        start: Option<u64>,
2436        end: Option<u64>,
2437        client_id: Option<ClientId>,
2438        params: Option<Py<PyDict>>,
2439    ) -> PyResult<String> {
2440        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2441            match params {
2442                Some(dict) => from_pydict(py, &dict),
2443                None => Ok(None),
2444            }
2445        })?;
2446        let start = start.map(|ts| UnixNanos::from(ts).to_datetime_utc());
2447        let end = end.map(|ts| UnixNanos::from(ts).to_datetime_utc());
2448
2449        let request_id = DataActor::request_instruments(
2450            self.inner_mut(),
2451            venue,
2452            start,
2453            end,
2454            client_id,
2455            params_map,
2456        )
2457        .map_err(to_pyvalue_err)?;
2458        Ok(request_id.to_string())
2459    }
2460
2461    #[pyo3(name = "request_book_snapshot")]
2462    #[pyo3(signature = (instrument_id, depth=None, client_id=None, params=None))]
2463    fn py_request_book_snapshot(
2464        &mut self,
2465        instrument_id: InstrumentId,
2466        depth: Option<usize>,
2467        client_id: Option<ClientId>,
2468        params: Option<Py<PyDict>>,
2469    ) -> PyResult<String> {
2470        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2471            match params {
2472                Some(dict) => from_pydict(py, &dict),
2473                None => Ok(None),
2474            }
2475        })?;
2476        let depth = depth.and_then(NonZeroUsize::new);
2477
2478        let request_id = DataActor::request_book_snapshot(
2479            self.inner_mut(),
2480            instrument_id,
2481            depth,
2482            client_id,
2483            params_map,
2484        )
2485        .map_err(to_pyvalue_err)?;
2486        Ok(request_id.to_string())
2487    }
2488
2489    #[pyo3(name = "request_quotes")]
2490    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None))]
2491    fn py_request_quotes(
2492        &mut self,
2493        instrument_id: InstrumentId,
2494        start: Option<u64>,
2495        end: Option<u64>,
2496        limit: Option<usize>,
2497        client_id: Option<ClientId>,
2498        params: Option<Py<PyDict>>,
2499    ) -> PyResult<String> {
2500        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2501            match params {
2502                Some(dict) => from_pydict(py, &dict),
2503                None => Ok(None),
2504            }
2505        })?;
2506        let limit = limit.and_then(NonZeroUsize::new);
2507        let start = start.map(|ts| UnixNanos::from(ts).to_datetime_utc());
2508        let end = end.map(|ts| UnixNanos::from(ts).to_datetime_utc());
2509
2510        let request_id = DataActor::request_quotes(
2511            self.inner_mut(),
2512            instrument_id,
2513            start,
2514            end,
2515            limit,
2516            client_id,
2517            params_map,
2518        )
2519        .map_err(to_pyvalue_err)?;
2520        Ok(request_id.to_string())
2521    }
2522
2523    #[pyo3(name = "request_trades")]
2524    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None))]
2525    fn py_request_trades(
2526        &mut self,
2527        instrument_id: InstrumentId,
2528        start: Option<u64>,
2529        end: Option<u64>,
2530        limit: Option<usize>,
2531        client_id: Option<ClientId>,
2532        params: Option<Py<PyDict>>,
2533    ) -> PyResult<String> {
2534        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2535            match params {
2536                Some(dict) => from_pydict(py, &dict),
2537                None => Ok(None),
2538            }
2539        })?;
2540        let limit = limit.and_then(NonZeroUsize::new);
2541        let start = start.map(|ts| UnixNanos::from(ts).to_datetime_utc());
2542        let end = end.map(|ts| UnixNanos::from(ts).to_datetime_utc());
2543
2544        let request_id = DataActor::request_trades(
2545            self.inner_mut(),
2546            instrument_id,
2547            start,
2548            end,
2549            limit,
2550            client_id,
2551            params_map,
2552        )
2553        .map_err(to_pyvalue_err)?;
2554        Ok(request_id.to_string())
2555    }
2556
2557    #[pyo3(name = "request_funding_rates")]
2558    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None))]
2559    fn py_request_funding_rates(
2560        &mut self,
2561        instrument_id: InstrumentId,
2562        start: Option<u64>,
2563        end: Option<u64>,
2564        limit: Option<usize>,
2565        client_id: Option<ClientId>,
2566        params: Option<Py<PyDict>>,
2567    ) -> PyResult<String> {
2568        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2569            match params {
2570                Some(dict) => from_pydict(py, &dict),
2571                None => Ok(None),
2572            }
2573        })?;
2574        let limit = limit.and_then(NonZeroUsize::new);
2575        let start = start.map(|ts| UnixNanos::from(ts).to_datetime_utc());
2576        let end = end.map(|ts| UnixNanos::from(ts).to_datetime_utc());
2577
2578        let request_id = DataActor::request_funding_rates(
2579            self.inner_mut(),
2580            instrument_id,
2581            start,
2582            end,
2583            limit,
2584            client_id,
2585            params_map,
2586        )
2587        .map_err(to_pyvalue_err)?;
2588        Ok(request_id.to_string())
2589    }
2590
2591    #[pyo3(name = "request_bars")]
2592    #[pyo3(signature = (bar_type, start=None, end=None, limit=None, client_id=None, params=None))]
2593    fn py_request_bars(
2594        &mut self,
2595        bar_type: BarType,
2596        start: Option<u64>,
2597        end: Option<u64>,
2598        limit: Option<usize>,
2599        client_id: Option<ClientId>,
2600        params: Option<Py<PyDict>>,
2601    ) -> PyResult<String> {
2602        let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2603            match params {
2604                Some(dict) => from_pydict(py, &dict),
2605                None => Ok(None),
2606            }
2607        })?;
2608        let limit = limit.and_then(NonZeroUsize::new);
2609        let start = start.map(|ts| UnixNanos::from(ts).to_datetime_utc());
2610        let end = end.map(|ts| UnixNanos::from(ts).to_datetime_utc());
2611
2612        let request_id = DataActor::request_bars(
2613            self.inner_mut(),
2614            bar_type,
2615            start,
2616            end,
2617            limit,
2618            client_id,
2619            params_map,
2620        )
2621        .map_err(to_pyvalue_err)?;
2622        Ok(request_id.to_string())
2623    }
2624}
2625
2626#[cfg(test)]
2627mod tests {
2628    use std::{cell::RefCell, rc::Rc, str::FromStr};
2629
2630    use nautilus_common::{
2631        actor::DataActor,
2632        cache::Cache,
2633        clock::{Clock, TestClock},
2634        signal::Signal,
2635        timer::TimeEvent,
2636    };
2637    use nautilus_core::{UUID4, UnixNanos};
2638    use nautilus_model::{
2639        data::{
2640            Bar, BarType, CustomData, FundingRateUpdate, IndexPriceUpdate, InstrumentStatus,
2641            MarkPriceUpdate, OrderBookDelta, OrderBookDeltas, QuoteTick, TradeTick,
2642            close::InstrumentClose,
2643            greeks::OptionGreekValues,
2644            option_chain::{OptionChainSlice, OptionGreeks},
2645            stubs::stub_custom_data,
2646        },
2647        enums::{
2648            AggressorSide, BookType, GreeksConvention, InstrumentCloseType, MarketStatusAction,
2649            OrderSide, PositionSide,
2650        },
2651        events::{
2652            OrderAccepted, OrderCancelRejected, OrderCanceled, OrderDenied, OrderEmulated,
2653            OrderExpired, OrderInitialized, OrderModifyRejected, OrderPendingCancel,
2654            OrderPendingUpdate, OrderRejected, OrderReleased, OrderSubmitted, OrderTriggered,
2655            OrderUpdated, PositionChanged, PositionClosed, PositionOpened,
2656            order::spec::OrderFilledSpec,
2657        },
2658        identifiers::{
2659            AccountId, ClientOrderId, InstrumentId, OptionSeriesId, PositionId, StrategyId,
2660            TradeId, TraderId, Venue,
2661        },
2662        instruments::{CurrencyPair, InstrumentAny, stubs::audusd_sim},
2663        orderbook::OrderBook,
2664        types::{Currency, Money, Price, Quantity},
2665    };
2666    use nautilus_portfolio::portfolio::Portfolio;
2667    use pyo3::{Py, PyAny, PyResult, Python, ffi::c_str, types::PyAnyMethods};
2668    use ustr::Ustr;
2669
2670    use super::PyStrategy;
2671    use crate::strategy::Strategy;
2672
2673    const TRACKING_STRATEGY_CODE: &std::ffi::CStr = c_str!(
2674        r#"
2675class TrackingStrategy:
2676    TRACKED_METHODS = {
2677        "on_start",
2678        "on_stop",
2679        "on_resume",
2680        "on_reset",
2681        "on_dispose",
2682        "on_degrade",
2683        "on_fault",
2684        "on_time_event",
2685        "on_data",
2686        "on_signal",
2687        "on_instrument",
2688        "on_quote",
2689        "on_trade",
2690        "on_bar",
2691        "on_book_deltas",
2692        "on_book",
2693        "on_mark_price",
2694        "on_index_price",
2695        "on_funding_rate",
2696        "on_instrument_status",
2697        "on_instrument_close",
2698        "on_option_greeks",
2699        "on_option_chain",
2700        "on_historical_data",
2701        "on_historical_quotes",
2702        "on_historical_trades",
2703        "on_historical_funding_rates",
2704        "on_historical_bars",
2705        "on_historical_mark_prices",
2706        "on_historical_index_prices",
2707        "on_order_initialized",
2708        "on_order_denied",
2709        "on_order_emulated",
2710        "on_order_released",
2711        "on_order_submitted",
2712        "on_order_rejected",
2713        "on_order_accepted",
2714        "on_order_expired",
2715        "on_order_triggered",
2716        "on_order_pending_update",
2717        "on_order_pending_cancel",
2718        "on_order_modify_rejected",
2719        "on_order_cancel_rejected",
2720        "on_order_updated",
2721        "on_order_canceled",
2722        "on_order_filled",
2723        "on_position_opened",
2724        "on_position_changed",
2725        "on_position_closed",
2726    }
2727
2728    def __init__(self):
2729        self.calls = []
2730
2731    def _record(self, method_name, *args):
2732        self.calls.append((method_name, args))
2733
2734    def was_called(self, method_name):
2735        return any(call[0] == method_name for call in self.calls)
2736
2737    def call_count(self, method_name):
2738        return sum(1 for call in self.calls if call[0] == method_name)
2739
2740    def __getattr__(self, name):
2741        if name in self.TRACKED_METHODS:
2742            return lambda *args: self._record(name, *args)
2743        raise AttributeError(name)
2744"#
2745    );
2746
2747    fn create_tracking_python_strategy(py: Python<'_>) -> PyResult<Py<PyAny>> {
2748        py.run(TRACKING_STRATEGY_CODE, None, None)?;
2749        let tracking_strategy_class = py.eval(c_str!("TrackingStrategy"), None, None)?;
2750        let instance = tracking_strategy_class.call0()?;
2751        Ok(instance.unbind())
2752    }
2753
2754    fn python_method_was_called(
2755        py_strategy: &Py<PyAny>,
2756        py: Python<'_>,
2757        method_name: &str,
2758    ) -> bool {
2759        py_strategy
2760            .call_method1(py, "was_called", (method_name,))
2761            .and_then(|result| result.extract::<bool>(py))
2762            .unwrap_or(false)
2763    }
2764
2765    fn python_method_call_count(py_strategy: &Py<PyAny>, py: Python<'_>, method_name: &str) -> i32 {
2766        py_strategy
2767            .call_method1(py, "call_count", (method_name,))
2768            .and_then(|result| result.extract::<i32>(py))
2769            .unwrap_or(0)
2770    }
2771
2772    fn sample_instrument() -> CurrencyPair {
2773        audusd_sim()
2774    }
2775
2776    fn sample_time_event() -> TimeEvent {
2777        TimeEvent::new(
2778            Ustr::from("test_timer"),
2779            UUID4::new(),
2780            UnixNanos::default(),
2781            UnixNanos::default(),
2782        )
2783    }
2784
2785    fn sample_data() -> CustomData {
2786        stub_custom_data(1, 42, None, None)
2787    }
2788
2789    fn sample_signal() -> Signal {
2790        Signal::new(
2791            Ustr::from("test_signal"),
2792            "1.0".to_string(),
2793            UnixNanos::default(),
2794            UnixNanos::default(),
2795        )
2796    }
2797
2798    fn sample_quote() -> QuoteTick {
2799        let instrument = sample_instrument();
2800        QuoteTick::new(
2801            instrument.id,
2802            Price::from("1.00000"),
2803            Price::from("1.00001"),
2804            Quantity::from(100_000),
2805            Quantity::from(100_000),
2806            UnixNanos::default(),
2807            UnixNanos::default(),
2808        )
2809    }
2810
2811    fn sample_trade() -> TradeTick {
2812        let instrument = sample_instrument();
2813        TradeTick::new(
2814            instrument.id,
2815            Price::from("1.00000"),
2816            Quantity::from(100_000),
2817            AggressorSide::Buyer,
2818            TradeId::new("123456"),
2819            UnixNanos::default(),
2820            UnixNanos::default(),
2821        )
2822    }
2823
2824    fn sample_bar() -> Bar {
2825        let instrument = sample_instrument();
2826        let bar_type =
2827            BarType::from_str(&format!("{}-1-MINUTE-LAST-INTERNAL", instrument.id)).unwrap();
2828        Bar::new(
2829            bar_type,
2830            Price::from("1.00000"),
2831            Price::from("1.00010"),
2832            Price::from("0.99990"),
2833            Price::from("1.00005"),
2834            Quantity::from(100_000),
2835            UnixNanos::default(),
2836            UnixNanos::default(),
2837        )
2838    }
2839
2840    fn sample_book() -> OrderBook {
2841        OrderBook::new(sample_instrument().id, BookType::L2_MBP)
2842    }
2843
2844    fn sample_book_deltas() -> OrderBookDeltas {
2845        let instrument = sample_instrument();
2846        let delta =
2847            OrderBookDelta::clear(instrument.id, 0, UnixNanos::default(), UnixNanos::default());
2848        OrderBookDeltas::new(instrument.id, vec![delta])
2849    }
2850
2851    fn sample_mark_price() -> MarkPriceUpdate {
2852        MarkPriceUpdate::new(
2853            sample_instrument().id,
2854            Price::from("1.00000"),
2855            UnixNanos::default(),
2856            UnixNanos::default(),
2857        )
2858    }
2859
2860    fn sample_index_price() -> IndexPriceUpdate {
2861        IndexPriceUpdate::new(
2862            sample_instrument().id,
2863            Price::from("1.00000"),
2864            UnixNanos::default(),
2865            UnixNanos::default(),
2866        )
2867    }
2868
2869    fn sample_funding_rate() -> FundingRateUpdate {
2870        FundingRateUpdate::new(
2871            sample_instrument().id,
2872            "0.0001".parse().unwrap(),
2873            None,
2874            None,
2875            UnixNanos::default(),
2876            UnixNanos::default(),
2877        )
2878    }
2879
2880    fn sample_instrument_status() -> InstrumentStatus {
2881        InstrumentStatus::new(
2882            sample_instrument().id,
2883            MarketStatusAction::Trading,
2884            UnixNanos::default(),
2885            UnixNanos::default(),
2886            None,
2887            None,
2888            None,
2889            None,
2890            None,
2891        )
2892    }
2893
2894    fn sample_instrument_close() -> InstrumentClose {
2895        InstrumentClose::new(
2896            sample_instrument().id,
2897            Price::from("1.00000"),
2898            InstrumentCloseType::EndOfSession,
2899            UnixNanos::default(),
2900            UnixNanos::default(),
2901        )
2902    }
2903
2904    fn sample_option_greeks() -> OptionGreeks {
2905        OptionGreeks {
2906            instrument_id: InstrumentId::from("AUD/USD.SIM"),
2907            convention: GreeksConvention::BlackScholes,
2908            greeks: OptionGreekValues {
2909                delta: 0.55,
2910                gamma: 0.03,
2911                vega: 0.12,
2912                theta: -0.05,
2913                rho: 0.01,
2914            },
2915            mark_iv: Some(0.25),
2916            bid_iv: None,
2917            ask_iv: None,
2918            underlying_price: None,
2919            open_interest: None,
2920            ts_event: UnixNanos::default(),
2921            ts_init: UnixNanos::default(),
2922        }
2923    }
2924
2925    fn sample_option_chain() -> OptionChainSlice {
2926        OptionChainSlice {
2927            series_id: OptionSeriesId::new(
2928                Venue::from("SIM"),
2929                Ustr::from("AUD"),
2930                Ustr::from("USD"),
2931                UnixNanos::from(1_711_036_800_000_000_000),
2932            ),
2933            atm_strike: None,
2934            calls: Default::default(),
2935            puts: Default::default(),
2936            ts_event: UnixNanos::default(),
2937            ts_init: UnixNanos::default(),
2938        }
2939    }
2940
2941    fn sample_position_opened() -> PositionOpened {
2942        PositionOpened {
2943            trader_id: TraderId::from("TRADER-001"),
2944            strategy_id: StrategyId::from("TEST-001"),
2945            instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
2946            position_id: PositionId::from("P-001"),
2947            account_id: AccountId::from("ACC-001"),
2948            opening_order_id: ClientOrderId::from("O-001"),
2949            entry: OrderSide::Buy,
2950            side: PositionSide::Long,
2951            signed_qty: 1.0,
2952            quantity: Quantity::from(1),
2953            last_qty: Quantity::from(1),
2954            last_px: Price::from("1.00000"),
2955            currency: Currency::from("USD"),
2956            avg_px_open: 1.0,
2957            event_id: UUID4::new(),
2958            ts_event: UnixNanos::default(),
2959            ts_init: UnixNanos::default(),
2960        }
2961    }
2962
2963    fn sample_position_changed() -> PositionChanged {
2964        PositionChanged {
2965            trader_id: TraderId::from("TRADER-001"),
2966            strategy_id: StrategyId::from("TEST-001"),
2967            instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
2968            position_id: PositionId::from("P-001"),
2969            account_id: AccountId::from("ACC-001"),
2970            opening_order_id: ClientOrderId::from("O-001"),
2971            entry: OrderSide::Buy,
2972            side: PositionSide::Long,
2973            signed_qty: 2.0,
2974            quantity: Quantity::from(2),
2975            peak_quantity: Quantity::from(2),
2976            last_qty: Quantity::from(1),
2977            last_px: Price::from("1.10000"),
2978            currency: Currency::from("USD"),
2979            avg_px_open: 1.05,
2980            avg_px_close: None,
2981            realized_return: 0.0,
2982            realized_pnl: None,
2983            unrealized_pnl: Money::new(0.0, Currency::USD()),
2984            event_id: UUID4::new(),
2985            ts_opened: UnixNanos::default(),
2986            ts_event: UnixNanos::default(),
2987            ts_init: UnixNanos::default(),
2988        }
2989    }
2990
2991    fn sample_position_closed() -> PositionClosed {
2992        PositionClosed {
2993            trader_id: TraderId::from("TRADER-001"),
2994            strategy_id: StrategyId::from("TEST-001"),
2995            instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
2996            position_id: PositionId::from("P-001"),
2997            account_id: AccountId::from("ACC-001"),
2998            opening_order_id: ClientOrderId::from("O-001"),
2999            closing_order_id: Some(ClientOrderId::from("O-002")),
3000            entry: OrderSide::Buy,
3001            side: PositionSide::Flat,
3002            signed_qty: 0.0,
3003            quantity: Quantity::from(0),
3004            peak_quantity: Quantity::from(2),
3005            last_qty: Quantity::from(2),
3006            last_px: Price::from("1.20000"),
3007            currency: Currency::from("USD"),
3008            avg_px_open: 1.05,
3009            avg_px_close: Some(1.20),
3010            realized_return: 0.1,
3011            realized_pnl: Some(Money::new(0.1, Currency::USD())),
3012            unrealized_pnl: Money::new(0.0, Currency::USD()),
3013            duration: 1,
3014            event_id: UUID4::new(),
3015            ts_opened: UnixNanos::default(),
3016            ts_closed: Some(UnixNanos::default()),
3017            ts_event: UnixNanos::default(),
3018            ts_init: UnixNanos::default(),
3019        }
3020    }
3021
3022    fn create_registered_tracking_strategy(py: Python<'_>) -> (Py<PyAny>, PyStrategy) {
3023        let py_strategy = create_tracking_python_strategy(py).unwrap();
3024        let mut rust_strategy = PyStrategy::new(None);
3025        rust_strategy.set_python_instance(py_strategy.clone_ref(py));
3026
3027        let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(TestClock::new()));
3028        let cache = Rc::new(RefCell::new(Cache::new(None, None)));
3029        let portfolio = Rc::new(RefCell::new(Portfolio::new(
3030            cache.clone(),
3031            clock.clone(),
3032            None,
3033        )));
3034
3035        rust_strategy
3036            .register(TraderId::from("TRADER-001"), clock, cache, portfolio)
3037            .unwrap();
3038
3039        (py_strategy, rust_strategy)
3040    }
3041
3042    fn assert_python_dispatch<F>(py: Python<'_>, method_name: &str, invoke: F)
3043    where
3044        F: FnOnce(&mut PyStrategy) -> anyhow::Result<()>,
3045    {
3046        let (py_strategy, mut rust_strategy) = create_registered_tracking_strategy(py);
3047        let result = invoke(&mut rust_strategy);
3048
3049        assert!(result.is_ok());
3050        assert!(python_method_was_called(&py_strategy, py, method_name));
3051        assert_eq!(python_method_call_count(&py_strategy, py, method_name), 1);
3052    }
3053
3054    #[rstest::rstest]
3055    #[case("on_start")]
3056    #[case("on_stop")]
3057    #[case("on_resume")]
3058    #[case("on_reset")]
3059    #[case("on_dispose")]
3060    #[case("on_degrade")]
3061    #[case("on_fault")]
3062    fn test_python_dispatch_lifecycle_matrix(#[case] method_name: &str) {
3063        pyo3::Python::initialize();
3064        Python::attach(|py| {
3065            assert_python_dispatch(py, method_name, |rust_strategy| match method_name {
3066                "on_start" => DataActor::on_start(rust_strategy.inner_mut()),
3067                "on_stop" => DataActor::on_stop(rust_strategy.inner_mut()),
3068                "on_resume" => DataActor::on_resume(rust_strategy.inner_mut()),
3069                "on_reset" => DataActor::on_reset(rust_strategy.inner_mut()),
3070                "on_dispose" => DataActor::on_dispose(rust_strategy.inner_mut()),
3071                "on_degrade" => DataActor::on_degrade(rust_strategy.inner_mut()),
3072                "on_fault" => DataActor::on_fault(rust_strategy.inner_mut()),
3073                _ => unreachable!("unhandled lifecycle case: {method_name}"),
3074            });
3075        });
3076    }
3077
3078    #[rstest::rstest]
3079    #[case("on_time_event")]
3080    #[case("on_data")]
3081    #[case("on_signal")]
3082    #[case("on_instrument")]
3083    #[case("on_quote")]
3084    #[case("on_trade")]
3085    #[case("on_bar")]
3086    #[case("on_book_deltas")]
3087    #[case("on_book")]
3088    #[case("on_mark_price")]
3089    #[case("on_index_price")]
3090    #[case("on_funding_rate")]
3091    #[case("on_instrument_status")]
3092    #[case("on_instrument_close")]
3093    #[case("on_option_greeks")]
3094    #[case("on_option_chain")]
3095    #[case("on_historical_data")]
3096    #[case("on_historical_quotes")]
3097    #[case("on_historical_trades")]
3098    #[case("on_historical_funding_rates")]
3099    #[case("on_historical_bars")]
3100    #[case("on_historical_mark_prices")]
3101    #[case("on_historical_index_prices")]
3102    fn test_python_dispatch_data_callback_matrix(#[case] method_name: &str) {
3103        pyo3::Python::initialize();
3104        Python::attach(|py| {
3105            assert_python_dispatch(py, method_name, |rust_strategy| match method_name {
3106                "on_time_event" => {
3107                    let event = sample_time_event();
3108                    DataActor::on_time_event(rust_strategy.inner_mut(), &event)
3109                }
3110                "on_data" => {
3111                    let data = sample_data();
3112                    rust_strategy.inner_mut().on_data(&data)
3113                }
3114                "on_signal" => {
3115                    let signal = sample_signal();
3116                    rust_strategy.inner_mut().on_signal(&signal)
3117                }
3118                "on_instrument" => {
3119                    let instrument = InstrumentAny::CurrencyPair(sample_instrument());
3120                    rust_strategy.inner_mut().on_instrument(&instrument)
3121                }
3122                "on_quote" => {
3123                    let quote = sample_quote();
3124                    rust_strategy.inner_mut().on_quote(&quote)
3125                }
3126                "on_trade" => {
3127                    let trade = sample_trade();
3128                    rust_strategy.inner_mut().on_trade(&trade)
3129                }
3130                "on_bar" => {
3131                    let bar = sample_bar();
3132                    rust_strategy.inner_mut().on_bar(&bar)
3133                }
3134                "on_book_deltas" => {
3135                    let deltas = sample_book_deltas();
3136                    rust_strategy.inner_mut().on_book_deltas(&deltas)
3137                }
3138                "on_book" => {
3139                    let book = sample_book();
3140                    rust_strategy.inner_mut().on_book(&book)
3141                }
3142                "on_mark_price" => {
3143                    let mark_price = sample_mark_price();
3144                    rust_strategy.inner_mut().on_mark_price(&mark_price)
3145                }
3146                "on_index_price" => {
3147                    let index_price = sample_index_price();
3148                    rust_strategy.inner_mut().on_index_price(&index_price)
3149                }
3150                "on_funding_rate" => {
3151                    let funding_rate = sample_funding_rate();
3152                    rust_strategy.inner_mut().on_funding_rate(&funding_rate)
3153                }
3154                "on_instrument_status" => {
3155                    let status = sample_instrument_status();
3156                    rust_strategy.inner_mut().on_instrument_status(&status)
3157                }
3158                "on_instrument_close" => {
3159                    let close = sample_instrument_close();
3160                    rust_strategy.inner_mut().on_instrument_close(&close)
3161                }
3162                "on_option_greeks" => {
3163                    let greeks = sample_option_greeks();
3164                    DataActor::on_option_greeks(rust_strategy.inner_mut(), &greeks)
3165                }
3166                "on_option_chain" => {
3167                    let slice = sample_option_chain();
3168                    DataActor::on_option_chain(rust_strategy.inner_mut(), &slice)
3169                }
3170                "on_historical_data" => {
3171                    let data = sample_data();
3172                    rust_strategy.inner_mut().on_historical_data(&data)
3173                }
3174                "on_historical_quotes" => {
3175                    let quotes = vec![sample_quote()];
3176                    rust_strategy.inner_mut().on_historical_quotes(&quotes)
3177                }
3178                "on_historical_trades" => {
3179                    let trades = vec![sample_trade()];
3180                    rust_strategy.inner_mut().on_historical_trades(&trades)
3181                }
3182                "on_historical_funding_rates" => {
3183                    let funding_rates = vec![sample_funding_rate()];
3184                    rust_strategy
3185                        .inner_mut()
3186                        .on_historical_funding_rates(&funding_rates)
3187                }
3188                "on_historical_bars" => {
3189                    let bars = vec![sample_bar()];
3190                    rust_strategy.inner_mut().on_historical_bars(&bars)
3191                }
3192                "on_historical_mark_prices" => {
3193                    let mark_prices = vec![sample_mark_price()];
3194                    rust_strategy
3195                        .inner_mut()
3196                        .on_historical_mark_prices(&mark_prices)
3197                }
3198                "on_historical_index_prices" => {
3199                    let index_prices = vec![sample_index_price()];
3200                    rust_strategy
3201                        .inner_mut()
3202                        .on_historical_index_prices(&index_prices)
3203                }
3204                _ => unreachable!("unhandled data callback case: {method_name}"),
3205            });
3206        });
3207    }
3208
3209    #[rstest::rstest]
3210    #[case("on_order_initialized")]
3211    #[case("on_order_denied")]
3212    #[case("on_order_emulated")]
3213    #[case("on_order_released")]
3214    #[case("on_order_submitted")]
3215    #[case("on_order_rejected")]
3216    #[case("on_order_accepted")]
3217    #[case("on_order_expired")]
3218    #[case("on_order_triggered")]
3219    #[case("on_order_pending_update")]
3220    #[case("on_order_pending_cancel")]
3221    #[case("on_order_modify_rejected")]
3222    #[case("on_order_cancel_rejected")]
3223    #[case("on_order_updated")]
3224    #[case("on_order_canceled")]
3225    #[case("on_order_filled")]
3226    fn test_python_dispatch_order_callback_matrix(#[case] method_name: &str) {
3227        pyo3::Python::initialize();
3228        Python::attach(|py| {
3229            assert_python_dispatch(py, method_name, |rust_strategy| match method_name {
3230                "on_order_initialized" => {
3231                    Strategy::on_order_initialized(
3232                        rust_strategy.inner_mut(),
3233                        OrderInitialized::default(),
3234                    );
3235                    Ok(())
3236                }
3237                "on_order_denied" => {
3238                    Strategy::on_order_denied(rust_strategy.inner_mut(), OrderDenied::default());
3239                    Ok(())
3240                }
3241                "on_order_emulated" => {
3242                    Strategy::on_order_emulated(
3243                        rust_strategy.inner_mut(),
3244                        OrderEmulated::default(),
3245                    );
3246                    Ok(())
3247                }
3248                "on_order_released" => {
3249                    Strategy::on_order_released(
3250                        rust_strategy.inner_mut(),
3251                        OrderReleased::default(),
3252                    );
3253                    Ok(())
3254                }
3255                "on_order_submitted" => {
3256                    Strategy::on_order_submitted(
3257                        rust_strategy.inner_mut(),
3258                        OrderSubmitted::default(),
3259                    );
3260                    Ok(())
3261                }
3262                "on_order_rejected" => {
3263                    Strategy::on_order_rejected(
3264                        rust_strategy.inner_mut(),
3265                        OrderRejected::default(),
3266                    );
3267                    Ok(())
3268                }
3269                "on_order_accepted" => {
3270                    Strategy::on_order_accepted(
3271                        rust_strategy.inner_mut(),
3272                        OrderAccepted::default(),
3273                    );
3274                    Ok(())
3275                }
3276                "on_order_expired" => {
3277                    Strategy::on_order_expired(rust_strategy.inner_mut(), OrderExpired::default());
3278                    Ok(())
3279                }
3280                "on_order_triggered" => {
3281                    Strategy::on_order_triggered(
3282                        rust_strategy.inner_mut(),
3283                        OrderTriggered::default(),
3284                    );
3285                    Ok(())
3286                }
3287                "on_order_pending_update" => {
3288                    Strategy::on_order_pending_update(
3289                        rust_strategy.inner_mut(),
3290                        OrderPendingUpdate::default(),
3291                    );
3292                    Ok(())
3293                }
3294                "on_order_pending_cancel" => {
3295                    Strategy::on_order_pending_cancel(
3296                        rust_strategy.inner_mut(),
3297                        OrderPendingCancel::default(),
3298                    );
3299                    Ok(())
3300                }
3301                "on_order_modify_rejected" => {
3302                    Strategy::on_order_modify_rejected(
3303                        rust_strategy.inner_mut(),
3304                        OrderModifyRejected::default(),
3305                    );
3306                    Ok(())
3307                }
3308                "on_order_cancel_rejected" => {
3309                    Strategy::on_order_cancel_rejected(
3310                        rust_strategy.inner_mut(),
3311                        OrderCancelRejected::default(),
3312                    );
3313                    Ok(())
3314                }
3315                "on_order_updated" => {
3316                    Strategy::on_order_updated(rust_strategy.inner_mut(), OrderUpdated::default());
3317                    Ok(())
3318                }
3319                "on_order_canceled" => {
3320                    let event = OrderCanceled::default();
3321                    DataActor::on_order_canceled(rust_strategy.inner_mut(), &event)
3322                }
3323                "on_order_filled" => {
3324                    let event = OrderFilledSpec::builder().build();
3325                    DataActor::on_order_filled(rust_strategy.inner_mut(), &event)
3326                }
3327                _ => unreachable!("unhandled order callback case: {method_name}"),
3328            });
3329        });
3330    }
3331
3332    #[rstest::rstest]
3333    #[case("on_position_opened")]
3334    #[case("on_position_changed")]
3335    #[case("on_position_closed")]
3336    fn test_python_dispatch_position_callback_matrix(#[case] method_name: &str) {
3337        pyo3::Python::initialize();
3338        Python::attach(|py| {
3339            assert_python_dispatch(py, method_name, |rust_strategy| match method_name {
3340                "on_position_opened" => {
3341                    Strategy::on_position_opened(
3342                        rust_strategy.inner_mut(),
3343                        sample_position_opened(),
3344                    );
3345                    Ok(())
3346                }
3347                "on_position_changed" => {
3348                    Strategy::on_position_changed(
3349                        rust_strategy.inner_mut(),
3350                        sample_position_changed(),
3351                    );
3352                    Ok(())
3353                }
3354                "on_position_closed" => {
3355                    Strategy::on_position_closed(
3356                        rust_strategy.inner_mut(),
3357                        sample_position_closed(),
3358                    );
3359                    Ok(())
3360                }
3361                _ => unreachable!("unhandled position callback case: {method_name}"),
3362            });
3363        });
3364    }
3365}