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