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    fmt::Debug,
22    num::NonZeroUsize,
23    ops::{Deref, DerefMut},
24    rc::Rc,
25};
26
27use indexmap::IndexMap;
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    nanos::UnixNanos,
44    python::{IntoPyObjectNautilusExt, to_pyruntime_err, to_pyvalue_err},
45};
46use nautilus_model::{
47    data::{
48        Bar, BarType, DataType, FundingRateUpdate, IndexPriceUpdate, InstrumentStatus,
49        MarkPriceUpdate, OrderBookDeltas, QuoteTick, TradeTick, close::InstrumentClose,
50    },
51    enums::{BookType, OmsType, OrderSide, PositionSide, TimeInForce},
52    events::{
53        OrderAccepted, OrderCancelRejected, OrderDenied, OrderEmulated, OrderExpired,
54        OrderInitialized, OrderModifyRejected, OrderPendingCancel, OrderPendingUpdate,
55        OrderRejected, OrderReleased, OrderSubmitted, OrderTriggered, OrderUpdated,
56        PositionChanged, PositionClosed, PositionOpened,
57    },
58    identifiers::{AccountId, ClientId, InstrumentId, PositionId, StrategyId, TraderId, Venue},
59    instruments::InstrumentAny,
60    orderbook::OrderBook,
61    orders::OrderAny,
62    position::Position,
63    python::{instruments::instrument_any_to_pyobject, orders::pyobject_to_order_any},
64    types::{Price, Quantity},
65};
66use nautilus_portfolio::portfolio::Portfolio;
67use pyo3::prelude::*;
68use ustr::Ustr;
69
70use crate::strategy::{Strategy, StrategyConfig, StrategyCore};
71
72#[pyo3::pymethods]
73impl StrategyConfig {
74    #[new]
75    #[pyo3(signature = (
76        strategy_id=None,
77        order_id_tag=None,
78        oms_type=None,
79        external_order_claims=None,
80        manage_contingent_orders=false,
81        manage_gtd_expiry=false,
82        manage_stop=false,
83        market_exit_interval_ms=100,
84        market_exit_max_attempts=100,
85        market_exit_time_in_force=TimeInForce::Gtc,
86        market_exit_reduce_only=true,
87        use_uuid_client_order_ids=false,
88        use_hyphens_in_client_order_ids=true,
89        log_events=true,
90        log_commands=true,
91        log_rejected_due_post_only_as_warning=true
92    ))]
93    #[allow(clippy::too_many_arguments)]
94    fn py_new(
95        strategy_id: Option<StrategyId>,
96        order_id_tag: Option<String>,
97        oms_type: Option<OmsType>,
98        external_order_claims: Option<Vec<InstrumentId>>,
99        manage_contingent_orders: bool,
100        manage_gtd_expiry: bool,
101        manage_stop: bool,
102        market_exit_interval_ms: u64,
103        market_exit_max_attempts: u64,
104        market_exit_time_in_force: TimeInForce,
105        market_exit_reduce_only: bool,
106        use_uuid_client_order_ids: bool,
107        use_hyphens_in_client_order_ids: bool,
108        log_events: bool,
109        log_commands: bool,
110        log_rejected_due_post_only_as_warning: bool,
111    ) -> Self {
112        Self {
113            strategy_id,
114            order_id_tag,
115            use_uuid_client_order_ids,
116            use_hyphens_in_client_order_ids,
117            oms_type,
118            external_order_claims,
119            manage_contingent_orders,
120            manage_gtd_expiry,
121            manage_stop,
122            market_exit_interval_ms,
123            market_exit_max_attempts,
124            market_exit_time_in_force,
125            market_exit_reduce_only,
126            log_events,
127            log_commands,
128            log_rejected_due_post_only_as_warning,
129        }
130    }
131
132    #[getter]
133    fn strategy_id(&self) -> Option<StrategyId> {
134        self.strategy_id
135    }
136
137    #[getter]
138    fn order_id_tag(&self) -> Option<&String> {
139        self.order_id_tag.as_ref()
140    }
141
142    #[getter]
143    fn oms_type(&self) -> Option<OmsType> {
144        self.oms_type
145    }
146
147    #[getter]
148    fn manage_contingent_orders(&self) -> bool {
149        self.manage_contingent_orders
150    }
151
152    #[getter]
153    fn manage_gtd_expiry(&self) -> bool {
154        self.manage_gtd_expiry
155    }
156
157    #[getter]
158    fn use_uuid_client_order_ids(&self) -> bool {
159        self.use_uuid_client_order_ids
160    }
161
162    #[getter]
163    fn use_hyphens_in_client_order_ids(&self) -> bool {
164        self.use_hyphens_in_client_order_ids
165    }
166
167    #[getter]
168    fn log_events(&self) -> bool {
169        self.log_events
170    }
171
172    #[getter]
173    fn log_commands(&self) -> bool {
174        self.log_commands
175    }
176
177    #[getter]
178    fn log_rejected_due_post_only_as_warning(&self) -> bool {
179        self.log_rejected_due_post_only_as_warning
180    }
181}
182
183/// Inner state of PyStrategy, shared between Python wrapper and Rust registries.
184pub struct PyStrategyInner {
185    core: StrategyCore,
186    py_self: Option<Py<PyAny>>,
187    clock: PyClock,
188    logger: PyLogger,
189}
190
191impl Debug for PyStrategyInner {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        f.debug_struct(stringify!(PyStrategyInner))
194            .field("core", &self.core)
195            .field("py_self", &self.py_self.as_ref().map(|_| "<Py<PyAny>>"))
196            .field("clock", &self.clock)
197            .field("logger", &self.logger)
198            .finish()
199    }
200}
201
202impl PyStrategyInner {
203    fn dispatch_on_start(&self) -> PyResult<()> {
204        if let Some(ref py_self) = self.py_self {
205            Python::attach(|py| py_self.call_method0(py, "on_start"))?;
206        }
207        Ok(())
208    }
209
210    fn dispatch_on_stop(&self) -> PyResult<()> {
211        if let Some(ref py_self) = self.py_self {
212            Python::attach(|py| py_self.call_method0(py, "on_stop"))?;
213        }
214        Ok(())
215    }
216
217    fn dispatch_on_resume(&self) -> PyResult<()> {
218        if let Some(ref py_self) = self.py_self {
219            Python::attach(|py| py_self.call_method0(py, "on_resume"))?;
220        }
221        Ok(())
222    }
223
224    fn dispatch_on_reset(&self) -> PyResult<()> {
225        if let Some(ref py_self) = self.py_self {
226            Python::attach(|py| py_self.call_method0(py, "on_reset"))?;
227        }
228        Ok(())
229    }
230
231    fn dispatch_on_dispose(&self) -> PyResult<()> {
232        if let Some(ref py_self) = self.py_self {
233            Python::attach(|py| py_self.call_method0(py, "on_dispose"))?;
234        }
235        Ok(())
236    }
237
238    fn dispatch_on_degrade(&self) -> PyResult<()> {
239        if let Some(ref py_self) = self.py_self {
240            Python::attach(|py| py_self.call_method0(py, "on_degrade"))?;
241        }
242        Ok(())
243    }
244
245    fn dispatch_on_fault(&self) -> PyResult<()> {
246        if let Some(ref py_self) = self.py_self {
247            Python::attach(|py| py_self.call_method0(py, "on_fault"))?;
248        }
249        Ok(())
250    }
251
252    fn dispatch_on_time_event(&self, event: &TimeEvent) -> PyResult<()> {
253        if let Some(ref py_self) = self.py_self {
254            Python::attach(|py| {
255                py_self.call_method1(py, "on_time_event", (event.clone().into_py_any_unwrap(py),))
256            })?;
257        }
258        Ok(())
259    }
260
261    fn dispatch_on_order_initialized(&self, event: OrderInitialized) -> PyResult<()> {
262        if let Some(ref py_self) = self.py_self {
263            Python::attach(|py| {
264                py_self.call_method1(py, "on_order_initialized", (event.into_py_any_unwrap(py),))
265            })?;
266        }
267        Ok(())
268    }
269
270    fn dispatch_on_order_denied(&self, event: OrderDenied) -> PyResult<()> {
271        if let Some(ref py_self) = self.py_self {
272            Python::attach(|py| {
273                py_self.call_method1(py, "on_order_denied", (event.into_py_any_unwrap(py),))
274            })?;
275        }
276        Ok(())
277    }
278
279    fn dispatch_on_order_emulated(&self, event: OrderEmulated) -> PyResult<()> {
280        if let Some(ref py_self) = self.py_self {
281            Python::attach(|py| {
282                py_self.call_method1(py, "on_order_emulated", (event.into_py_any_unwrap(py),))
283            })?;
284        }
285        Ok(())
286    }
287
288    fn dispatch_on_order_released(&self, event: OrderReleased) -> PyResult<()> {
289        if let Some(ref py_self) = self.py_self {
290            Python::attach(|py| {
291                py_self.call_method1(py, "on_order_released", (event.into_py_any_unwrap(py),))
292            })?;
293        }
294        Ok(())
295    }
296
297    fn dispatch_on_order_submitted(&self, event: OrderSubmitted) -> PyResult<()> {
298        if let Some(ref py_self) = self.py_self {
299            Python::attach(|py| {
300                py_self.call_method1(py, "on_order_submitted", (event.into_py_any_unwrap(py),))
301            })?;
302        }
303        Ok(())
304    }
305
306    fn dispatch_on_order_rejected(&self, event: OrderRejected) -> PyResult<()> {
307        if let Some(ref py_self) = self.py_self {
308            Python::attach(|py| {
309                py_self.call_method1(py, "on_order_rejected", (event.into_py_any_unwrap(py),))
310            })?;
311        }
312        Ok(())
313    }
314
315    fn dispatch_on_order_accepted(&self, event: OrderAccepted) -> PyResult<()> {
316        if let Some(ref py_self) = self.py_self {
317            Python::attach(|py| {
318                py_self.call_method1(py, "on_order_accepted", (event.into_py_any_unwrap(py),))
319            })?;
320        }
321        Ok(())
322    }
323
324    fn dispatch_on_order_expired(&self, event: OrderExpired) -> PyResult<()> {
325        if let Some(ref py_self) = self.py_self {
326            Python::attach(|py| {
327                py_self.call_method1(py, "on_order_expired", (event.into_py_any_unwrap(py),))
328            })?;
329        }
330        Ok(())
331    }
332
333    fn dispatch_on_order_triggered(&self, event: OrderTriggered) -> PyResult<()> {
334        if let Some(ref py_self) = self.py_self {
335            Python::attach(|py| {
336                py_self.call_method1(py, "on_order_triggered", (event.into_py_any_unwrap(py),))
337            })?;
338        }
339        Ok(())
340    }
341
342    fn dispatch_on_order_pending_update(&self, event: OrderPendingUpdate) -> PyResult<()> {
343        if let Some(ref py_self) = self.py_self {
344            Python::attach(|py| {
345                py_self.call_method1(
346                    py,
347                    "on_order_pending_update",
348                    (event.into_py_any_unwrap(py),),
349                )
350            })?;
351        }
352        Ok(())
353    }
354
355    fn dispatch_on_order_pending_cancel(&self, event: OrderPendingCancel) -> PyResult<()> {
356        if let Some(ref py_self) = self.py_self {
357            Python::attach(|py| {
358                py_self.call_method1(
359                    py,
360                    "on_order_pending_cancel",
361                    (event.into_py_any_unwrap(py),),
362                )
363            })?;
364        }
365        Ok(())
366    }
367
368    fn dispatch_on_order_modify_rejected(&self, event: OrderModifyRejected) -> PyResult<()> {
369        if let Some(ref py_self) = self.py_self {
370            Python::attach(|py| {
371                py_self.call_method1(
372                    py,
373                    "on_order_modify_rejected",
374                    (event.into_py_any_unwrap(py),),
375                )
376            })?;
377        }
378        Ok(())
379    }
380
381    fn dispatch_on_order_cancel_rejected(&self, event: OrderCancelRejected) -> PyResult<()> {
382        if let Some(ref py_self) = self.py_self {
383            Python::attach(|py| {
384                py_self.call_method1(
385                    py,
386                    "on_order_cancel_rejected",
387                    (event.into_py_any_unwrap(py),),
388                )
389            })?;
390        }
391        Ok(())
392    }
393
394    fn dispatch_on_order_updated(&self, event: OrderUpdated) -> PyResult<()> {
395        if let Some(ref py_self) = self.py_self {
396            Python::attach(|py| {
397                py_self.call_method1(py, "on_order_updated", (event.into_py_any_unwrap(py),))
398            })?;
399        }
400        Ok(())
401    }
402
403    // TODO: Position events don't have PyO3 bindings yet, so these are stubbed
404    fn dispatch_on_position_opened(&self, _event: PositionOpened) -> PyResult<()> {
405        Ok(())
406    }
407
408    fn dispatch_on_position_changed(&self, _event: PositionChanged) -> PyResult<()> {
409        Ok(())
410    }
411
412    fn dispatch_on_position_closed(&self, _event: PositionClosed) -> PyResult<()> {
413        Ok(())
414    }
415
416    fn dispatch_on_data(&mut self, data: Py<PyAny>) -> PyResult<()> {
417        if let Some(ref py_self) = self.py_self {
418            Python::attach(|py| py_self.call_method1(py, "on_data", (data,)))?;
419        }
420        Ok(())
421    }
422
423    fn dispatch_on_signal(&mut self, signal: &Signal) -> PyResult<()> {
424        if let Some(ref py_self) = self.py_self {
425            Python::attach(|py| {
426                py_self.call_method1(py, "on_signal", (signal.clone().into_py_any_unwrap(py),))
427            })?;
428        }
429        Ok(())
430    }
431
432    fn dispatch_on_instrument(&mut self, instrument: Py<PyAny>) -> PyResult<()> {
433        if let Some(ref py_self) = self.py_self {
434            Python::attach(|py| py_self.call_method1(py, "on_instrument", (instrument,)))?;
435        }
436        Ok(())
437    }
438
439    fn dispatch_on_quote(&mut self, quote: QuoteTick) -> PyResult<()> {
440        if let Some(ref py_self) = self.py_self {
441            Python::attach(|py| {
442                py_self.call_method1(py, "on_quote", (quote.into_py_any_unwrap(py),))
443            })?;
444        }
445        Ok(())
446    }
447
448    fn dispatch_on_trade(&mut self, trade: TradeTick) -> PyResult<()> {
449        if let Some(ref py_self) = self.py_self {
450            Python::attach(|py| {
451                py_self.call_method1(py, "on_trade", (trade.into_py_any_unwrap(py),))
452            })?;
453        }
454        Ok(())
455    }
456
457    fn dispatch_on_bar(&mut self, bar: Bar) -> PyResult<()> {
458        if let Some(ref py_self) = self.py_self {
459            Python::attach(|py| py_self.call_method1(py, "on_bar", (bar.into_py_any_unwrap(py),)))?;
460        }
461        Ok(())
462    }
463
464    fn dispatch_on_book_deltas(&mut self, deltas: OrderBookDeltas) -> PyResult<()> {
465        if let Some(ref py_self) = self.py_self {
466            Python::attach(|py| {
467                py_self.call_method1(py, "on_book_deltas", (deltas.into_py_any_unwrap(py),))
468            })?;
469        }
470        Ok(())
471    }
472
473    fn dispatch_on_book(&mut self, book: &OrderBook) -> PyResult<()> {
474        if let Some(ref py_self) = self.py_self {
475            Python::attach(|py| {
476                py_self.call_method1(py, "on_book", (book.clone().into_py_any_unwrap(py),))
477            })?;
478        }
479        Ok(())
480    }
481
482    fn dispatch_on_mark_price(&mut self, mark_price: MarkPriceUpdate) -> PyResult<()> {
483        if let Some(ref py_self) = self.py_self {
484            Python::attach(|py| {
485                py_self.call_method1(py, "on_mark_price", (mark_price.into_py_any_unwrap(py),))
486            })?;
487        }
488        Ok(())
489    }
490
491    fn dispatch_on_index_price(&mut self, index_price: IndexPriceUpdate) -> PyResult<()> {
492        if let Some(ref py_self) = self.py_self {
493            Python::attach(|py| {
494                py_self.call_method1(py, "on_index_price", (index_price.into_py_any_unwrap(py),))
495            })?;
496        }
497        Ok(())
498    }
499
500    fn dispatch_on_funding_rate(&mut self, funding_rate: FundingRateUpdate) -> PyResult<()> {
501        if let Some(ref py_self) = self.py_self {
502            Python::attach(|py| {
503                py_self.call_method1(
504                    py,
505                    "on_funding_rate",
506                    (funding_rate.into_py_any_unwrap(py),),
507                )
508            })?;
509        }
510        Ok(())
511    }
512
513    fn dispatch_on_instrument_status(&mut self, data: InstrumentStatus) -> PyResult<()> {
514        if let Some(ref py_self) = self.py_self {
515            Python::attach(|py| {
516                py_self.call_method1(py, "on_instrument_status", (data.into_py_any_unwrap(py),))
517            })?;
518        }
519        Ok(())
520    }
521
522    fn dispatch_on_instrument_close(&mut self, update: InstrumentClose) -> PyResult<()> {
523        if let Some(ref py_self) = self.py_self {
524            Python::attach(|py| {
525                py_self.call_method1(py, "on_instrument_close", (update.into_py_any_unwrap(py),))
526            })?;
527        }
528        Ok(())
529    }
530}
531
532impl Deref for PyStrategyInner {
533    type Target = DataActorCore;
534
535    fn deref(&self) -> &Self::Target {
536        &self.core
537    }
538}
539
540impl DerefMut for PyStrategyInner {
541    fn deref_mut(&mut self) -> &mut Self::Target {
542        &mut self.core
543    }
544}
545
546impl Strategy for PyStrategyInner {
547    fn core(&self) -> &StrategyCore {
548        &self.core
549    }
550
551    fn core_mut(&mut self) -> &mut StrategyCore {
552        &mut self.core
553    }
554
555    fn on_order_initialized(&mut self, event: OrderInitialized) {
556        let _ = self.dispatch_on_order_initialized(event);
557    }
558
559    fn on_order_denied(&mut self, event: OrderDenied) {
560        let _ = self.dispatch_on_order_denied(event);
561    }
562
563    fn on_order_emulated(&mut self, event: OrderEmulated) {
564        let _ = self.dispatch_on_order_emulated(event);
565    }
566
567    fn on_order_released(&mut self, event: OrderReleased) {
568        let _ = self.dispatch_on_order_released(event);
569    }
570
571    fn on_order_submitted(&mut self, event: OrderSubmitted) {
572        let _ = self.dispatch_on_order_submitted(event);
573    }
574
575    fn on_order_rejected(&mut self, event: OrderRejected) {
576        let _ = self.dispatch_on_order_rejected(event);
577    }
578
579    fn on_order_accepted(&mut self, event: OrderAccepted) {
580        let _ = self.dispatch_on_order_accepted(event);
581    }
582
583    fn on_order_expired(&mut self, event: OrderExpired) {
584        let _ = self.dispatch_on_order_expired(event);
585    }
586
587    fn on_order_triggered(&mut self, event: OrderTriggered) {
588        let _ = self.dispatch_on_order_triggered(event);
589    }
590
591    fn on_order_pending_update(&mut self, event: OrderPendingUpdate) {
592        let _ = self.dispatch_on_order_pending_update(event);
593    }
594
595    fn on_order_pending_cancel(&mut self, event: OrderPendingCancel) {
596        let _ = self.dispatch_on_order_pending_cancel(event);
597    }
598
599    fn on_order_modify_rejected(&mut self, event: OrderModifyRejected) {
600        let _ = self.dispatch_on_order_modify_rejected(event);
601    }
602
603    fn on_order_cancel_rejected(&mut self, event: OrderCancelRejected) {
604        let _ = self.dispatch_on_order_cancel_rejected(event);
605    }
606
607    fn on_order_updated(&mut self, event: OrderUpdated) {
608        let _ = self.dispatch_on_order_updated(event);
609    }
610
611    fn on_position_opened(&mut self, event: PositionOpened) {
612        let _ = self.dispatch_on_position_opened(event);
613    }
614
615    fn on_position_changed(&mut self, event: PositionChanged) {
616        let _ = self.dispatch_on_position_changed(event);
617    }
618
619    fn on_position_closed(&mut self, event: PositionClosed) {
620        let _ = self.dispatch_on_position_closed(event);
621    }
622}
623
624impl DataActor for PyStrategyInner {
625    fn on_start(&mut self) -> anyhow::Result<()> {
626        Strategy::on_start(self)?;
627        self.dispatch_on_start()
628            .map_err(|e| anyhow::anyhow!("Python on_start failed: {e}"))
629    }
630
631    fn on_stop(&mut self) -> anyhow::Result<()> {
632        self.dispatch_on_stop()
633            .map_err(|e| anyhow::anyhow!("Python on_stop failed: {e}"))
634    }
635
636    fn on_resume(&mut self) -> anyhow::Result<()> {
637        self.dispatch_on_resume()
638            .map_err(|e| anyhow::anyhow!("Python on_resume failed: {e}"))
639    }
640
641    fn on_reset(&mut self) -> anyhow::Result<()> {
642        self.dispatch_on_reset()
643            .map_err(|e| anyhow::anyhow!("Python on_reset failed: {e}"))
644    }
645
646    fn on_dispose(&mut self) -> anyhow::Result<()> {
647        self.dispatch_on_dispose()
648            .map_err(|e| anyhow::anyhow!("Python on_dispose failed: {e}"))
649    }
650
651    fn on_degrade(&mut self) -> anyhow::Result<()> {
652        self.dispatch_on_degrade()
653            .map_err(|e| anyhow::anyhow!("Python on_degrade failed: {e}"))
654    }
655
656    fn on_fault(&mut self) -> anyhow::Result<()> {
657        self.dispatch_on_fault()
658            .map_err(|e| anyhow::anyhow!("Python on_fault failed: {e}"))
659    }
660
661    fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()> {
662        Strategy::on_time_event(self, event)?;
663        self.dispatch_on_time_event(event)
664            .map_err(|e| anyhow::anyhow!("Python on_time_event failed: {e}"))
665    }
666
667    #[allow(unused_variables)]
668    fn on_data(&mut self, data: &dyn Any) -> anyhow::Result<()> {
669        Python::attach(|py| {
670            let py_data = py.None();
671            self.dispatch_on_data(py_data)
672                .map_err(|e| anyhow::anyhow!("Python on_data failed: {e}"))
673        })
674    }
675
676    fn on_signal(&mut self, signal: &Signal) -> anyhow::Result<()> {
677        self.dispatch_on_signal(signal)
678            .map_err(|e| anyhow::anyhow!("Python on_signal failed: {e}"))
679    }
680
681    fn on_instrument(&mut self, instrument: &InstrumentAny) -> anyhow::Result<()> {
682        Python::attach(|py| {
683            let py_instrument = instrument_any_to_pyobject(py, instrument.clone())
684                .map_err(|e| anyhow::anyhow!("Failed to convert InstrumentAny to Python: {e}"))?;
685            self.dispatch_on_instrument(py_instrument)
686                .map_err(|e| anyhow::anyhow!("Python on_instrument failed: {e}"))
687        })
688    }
689
690    fn on_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
691        self.dispatch_on_quote(*quote)
692            .map_err(|e| anyhow::anyhow!("Python on_quote failed: {e}"))
693    }
694
695    fn on_trade(&mut self, tick: &TradeTick) -> anyhow::Result<()> {
696        self.dispatch_on_trade(*tick)
697            .map_err(|e| anyhow::anyhow!("Python on_trade failed: {e}"))
698    }
699
700    fn on_bar(&mut self, bar: &Bar) -> anyhow::Result<()> {
701        self.dispatch_on_bar(*bar)
702            .map_err(|e| anyhow::anyhow!("Python on_bar failed: {e}"))
703    }
704
705    fn on_book_deltas(&mut self, deltas: &OrderBookDeltas) -> anyhow::Result<()> {
706        self.dispatch_on_book_deltas(deltas.clone())
707            .map_err(|e| anyhow::anyhow!("Python on_book_deltas failed: {e}"))
708    }
709
710    fn on_book(&mut self, order_book: &OrderBook) -> anyhow::Result<()> {
711        self.dispatch_on_book(order_book)
712            .map_err(|e| anyhow::anyhow!("Python on_book failed: {e}"))
713    }
714
715    fn on_mark_price(&mut self, mark_price: &MarkPriceUpdate) -> anyhow::Result<()> {
716        self.dispatch_on_mark_price(*mark_price)
717            .map_err(|e| anyhow::anyhow!("Python on_mark_price failed: {e}"))
718    }
719
720    fn on_index_price(&mut self, index_price: &IndexPriceUpdate) -> anyhow::Result<()> {
721        self.dispatch_on_index_price(*index_price)
722            .map_err(|e| anyhow::anyhow!("Python on_index_price failed: {e}"))
723    }
724
725    fn on_funding_rate(&mut self, funding_rate: &FundingRateUpdate) -> anyhow::Result<()> {
726        self.dispatch_on_funding_rate(*funding_rate)
727            .map_err(|e| anyhow::anyhow!("Python on_funding_rate failed: {e}"))
728    }
729
730    fn on_instrument_status(&mut self, data: &InstrumentStatus) -> anyhow::Result<()> {
731        self.dispatch_on_instrument_status(*data)
732            .map_err(|e| anyhow::anyhow!("Python on_instrument_status failed: {e}"))
733    }
734
735    fn on_instrument_close(&mut self, update: &InstrumentClose) -> anyhow::Result<()> {
736        self.dispatch_on_instrument_close(*update)
737            .map_err(|e| anyhow::anyhow!("Python on_instrument_close failed: {e}"))
738    }
739}
740
741/// Python-facing wrapper for Strategy.
742#[allow(non_camel_case_types)]
743#[pyo3::pyclass(
744    module = "nautilus_trader.trading",
745    name = "Strategy",
746    unsendable,
747    subclass
748)]
749pub struct PyStrategy {
750    inner: Rc<UnsafeCell<PyStrategyInner>>,
751}
752
753impl Debug for PyStrategy {
754    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
755        f.debug_struct(stringify!(PyStrategy))
756            .field("inner", &self.inner())
757            .finish()
758    }
759}
760
761impl PyStrategy {
762    #[inline]
763    #[allow(unsafe_code)]
764    pub(crate) fn inner(&self) -> &PyStrategyInner {
765        unsafe { &*self.inner.get() }
766    }
767
768    #[inline]
769    #[allow(unsafe_code, clippy::mut_from_ref)]
770    pub(crate) fn inner_mut(&self) -> &mut PyStrategyInner {
771        unsafe { &mut *self.inner.get() }
772    }
773}
774
775impl PyStrategy {
776    /// Creates a new PyStrategy instance.
777    pub fn new(config: Option<StrategyConfig>) -> Self {
778        let config = config.unwrap_or_default();
779        let core = StrategyCore::new(config);
780        let clock = PyClock::new_test();
781        let logger = PyLogger::new(core.actor.actor_id.as_str());
782
783        let inner = PyStrategyInner {
784            core,
785            py_self: None,
786            clock,
787            logger,
788        };
789
790        Self {
791            inner: Rc::new(UnsafeCell::new(inner)),
792        }
793    }
794
795    /// Sets the Python instance reference for method dispatch.
796    pub fn set_python_instance(&mut self, py_obj: Py<PyAny>) {
797        self.inner_mut().py_self = Some(py_obj);
798    }
799
800    /// Returns a value indicating whether the strategy has been registered with a trader.
801    pub fn is_registered(&self) -> bool {
802        self.inner().core.actor.is_registered()
803    }
804
805    /// Register the strategy with a trader.
806    ///
807    /// # Errors
808    ///
809    /// Returns an error if registration fails.
810    pub fn register(
811        &mut self,
812        trader_id: TraderId,
813        clock: Rc<RefCell<dyn Clock>>,
814        cache: Rc<RefCell<Cache>>,
815        portfolio: Rc<RefCell<Portfolio>>,
816    ) -> anyhow::Result<()> {
817        let inner = self.inner_mut();
818        inner.core.register(trader_id, clock, cache, portfolio)?;
819
820        inner.clock = PyClock::from_rc(inner.core.actor.clock_rc());
821
822        let actor_id = inner.core.actor.actor_id.inner();
823        let callback = TimeEventCallback::from(move |event: TimeEvent| {
824            if let Some(mut strategy) = try_get_actor_unchecked::<PyStrategyInner>(&actor_id) {
825                if let Err(e) = DataActor::on_time_event(&mut *strategy, &event) {
826                    log::error!("Python time event handler failed for strategy {actor_id}: {e}");
827                }
828            } else {
829                log::error!("Strategy {actor_id} not found for time event handling");
830            }
831        });
832
833        inner.clock.inner_mut().register_default_handler(callback);
834
835        Component::initialize(inner)
836    }
837
838    /// Registers this strategy in the global component and actor registries.
839    pub fn register_in_global_registries(&self) {
840        let inner = self.inner();
841        let component_id = Component::component_id(inner).inner();
842        let actor_id = Actor::id(inner);
843
844        let inner_ref: Rc<UnsafeCell<PyStrategyInner>> = self.inner.clone();
845
846        let component_trait_ref: Rc<UnsafeCell<dyn Component>> = inner_ref.clone();
847        get_component_registry().insert(component_id, component_trait_ref);
848
849        let actor_trait_ref: Rc<UnsafeCell<dyn Actor>> = inner_ref;
850        get_actor_registry().insert(actor_id, actor_trait_ref);
851    }
852}
853
854#[pyo3::pymethods]
855impl PyStrategy {
856    #[new]
857    #[pyo3(signature = (config=None))]
858    fn py_new(config: Option<StrategyConfig>) -> Self {
859        Self::new(config)
860    }
861
862    /// Captures the Python self reference for Rust→Python event dispatch.
863    #[pyo3(signature = (config=None))]
864    #[allow(unused_variables)]
865    fn __init__(slf: &Bound<'_, Self>, config: Option<StrategyConfig>) {
866        let py_self: Py<PyAny> = slf.clone().unbind().into_any();
867        slf.borrow_mut().set_python_instance(py_self);
868    }
869
870    #[getter]
871    #[pyo3(name = "trader_id")]
872    fn py_trader_id(&self) -> Option<TraderId> {
873        self.inner().core.trader_id()
874    }
875
876    #[getter]
877    #[pyo3(name = "strategy_id")]
878    fn py_strategy_id(&self) -> StrategyId {
879        StrategyId::from(self.inner().core.actor.actor_id.inner().as_str())
880    }
881
882    #[getter]
883    #[pyo3(name = "clock")]
884    fn py_clock(&self) -> PyResult<PyClock> {
885        let inner = self.inner();
886        if inner.core.actor.is_registered() {
887            Ok(inner.clock.clone())
888        } else {
889            Err(to_pyruntime_err(
890                "Strategy must be registered with a trader before accessing clock",
891            ))
892        }
893    }
894
895    #[getter]
896    #[pyo3(name = "cache")]
897    fn py_cache(&self) -> PyResult<PyCache> {
898        let inner = self.inner();
899        if inner.core.actor.is_registered() {
900            Ok(PyCache::from_rc(inner.core.actor.cache_rc()))
901        } else {
902            Err(to_pyruntime_err(
903                "Strategy must be registered with a trader before accessing cache",
904            ))
905        }
906    }
907
908    #[getter]
909    #[pyo3(name = "log")]
910    fn py_log(&self) -> PyLogger {
911        self.inner().logger.clone()
912    }
913
914    #[pyo3(name = "state")]
915    fn py_state(&self) -> ComponentState {
916        self.inner().core.actor.state()
917    }
918
919    #[pyo3(name = "is_ready")]
920    fn py_is_ready(&self) -> bool {
921        Component::is_ready(self.inner())
922    }
923
924    #[pyo3(name = "is_running")]
925    fn py_is_running(&self) -> bool {
926        Component::is_running(self.inner())
927    }
928
929    #[pyo3(name = "is_stopped")]
930    fn py_is_stopped(&self) -> bool {
931        Component::is_stopped(self.inner())
932    }
933
934    #[pyo3(name = "is_disposed")]
935    fn py_is_disposed(&self) -> bool {
936        Component::is_disposed(self.inner())
937    }
938
939    #[pyo3(name = "is_degraded")]
940    fn py_is_degraded(&self) -> bool {
941        Component::is_degraded(self.inner())
942    }
943
944    #[pyo3(name = "is_faulted")]
945    fn py_is_faulted(&self) -> bool {
946        Component::is_faulted(self.inner())
947    }
948
949    #[pyo3(name = "start")]
950    fn py_start(&mut self) -> PyResult<()> {
951        Component::start(self.inner_mut()).map_err(to_pyruntime_err)
952    }
953
954    #[pyo3(name = "stop")]
955    fn py_stop(&mut self) -> PyResult<()> {
956        Component::stop(self.inner_mut()).map_err(to_pyruntime_err)
957    }
958
959    #[pyo3(name = "resume")]
960    fn py_resume(&mut self) -> PyResult<()> {
961        Component::resume(self.inner_mut()).map_err(to_pyruntime_err)
962    }
963
964    #[pyo3(name = "reset")]
965    fn py_reset(&mut self) -> PyResult<()> {
966        Component::reset(self.inner_mut()).map_err(to_pyruntime_err)
967    }
968
969    #[pyo3(name = "dispose")]
970    fn py_dispose(&mut self) -> PyResult<()> {
971        Component::dispose(self.inner_mut()).map_err(to_pyruntime_err)
972    }
973
974    #[pyo3(name = "degrade")]
975    fn py_degrade(&mut self) -> PyResult<()> {
976        Component::degrade(self.inner_mut()).map_err(to_pyruntime_err)
977    }
978
979    #[pyo3(name = "fault")]
980    fn py_fault(&mut self) -> PyResult<()> {
981        Component::fault(self.inner_mut()).map_err(to_pyruntime_err)
982    }
983
984    #[pyo3(name = "submit_order")]
985    #[pyo3(signature = (order, position_id=None, client_id=None, params=None))]
986    fn py_submit_order(
987        &mut self,
988        py: Python<'_>,
989        order: Py<PyAny>,
990        position_id: Option<PositionId>,
991        client_id: Option<ClientId>,
992        params: Option<IndexMap<String, String>>,
993    ) -> PyResult<()> {
994        let order = pyobject_to_order_any(py, order)?;
995        let inner = self.inner_mut();
996        match params {
997            Some(p) => Strategy::submit_order_with_params(inner, order, position_id, client_id, p),
998            None => Strategy::submit_order(inner, order, position_id, client_id),
999        }
1000        .map_err(to_pyruntime_err)
1001    }
1002
1003    #[pyo3(name = "modify_order")]
1004    #[pyo3(signature = (order, quantity=None, price=None, trigger_price=None, client_id=None, params=None))]
1005    #[allow(clippy::too_many_arguments)]
1006    fn py_modify_order(
1007        &mut self,
1008        py: Python<'_>,
1009        order: Py<PyAny>,
1010        quantity: Option<Quantity>,
1011        price: Option<Price>,
1012        trigger_price: Option<Price>,
1013        client_id: Option<ClientId>,
1014        params: Option<IndexMap<String, String>>,
1015    ) -> PyResult<()> {
1016        let order = pyobject_to_order_any(py, order)?;
1017        let inner = self.inner_mut();
1018        match params {
1019            Some(p) => Strategy::modify_order_with_params(
1020                inner,
1021                order,
1022                quantity,
1023                price,
1024                trigger_price,
1025                client_id,
1026                p,
1027            ),
1028            None => Strategy::modify_order(inner, order, quantity, price, trigger_price, client_id),
1029        }
1030        .map_err(to_pyruntime_err)
1031    }
1032
1033    #[pyo3(name = "cancel_order")]
1034    #[pyo3(signature = (order, client_id=None, params=None))]
1035    fn py_cancel_order(
1036        &mut self,
1037        py: Python<'_>,
1038        order: Py<PyAny>,
1039        client_id: Option<ClientId>,
1040        params: Option<IndexMap<String, String>>,
1041    ) -> PyResult<()> {
1042        let order = pyobject_to_order_any(py, order)?;
1043        let inner = self.inner_mut();
1044        match params {
1045            Some(p) => Strategy::cancel_order_with_params(inner, order, client_id, p),
1046            None => Strategy::cancel_order(inner, order, client_id),
1047        }
1048        .map_err(to_pyruntime_err)
1049    }
1050
1051    #[pyo3(name = "cancel_orders")]
1052    #[pyo3(signature = (orders, client_id=None, params=None))]
1053    fn py_cancel_orders(
1054        &mut self,
1055        py: Python<'_>,
1056        orders: Vec<Py<PyAny>>,
1057        client_id: Option<ClientId>,
1058        params: Option<IndexMap<String, String>>,
1059    ) -> PyResult<()> {
1060        let orders: Vec<OrderAny> = orders
1061            .into_iter()
1062            .map(|o| pyobject_to_order_any(py, o))
1063            .collect::<PyResult<Vec<_>>>()?;
1064        Strategy::cancel_orders(self.inner_mut(), orders, client_id, params)
1065            .map_err(to_pyruntime_err)
1066    }
1067
1068    #[pyo3(name = "cancel_all_orders")]
1069    #[pyo3(signature = (instrument_id, order_side=None, client_id=None, params=None))]
1070    fn py_cancel_all_orders(
1071        &mut self,
1072        instrument_id: InstrumentId,
1073        order_side: Option<OrderSide>,
1074        client_id: Option<ClientId>,
1075        params: Option<IndexMap<String, String>>,
1076    ) -> PyResult<()> {
1077        let inner = self.inner_mut();
1078        match params {
1079            Some(p) => Strategy::cancel_all_orders_with_params(
1080                inner,
1081                instrument_id,
1082                order_side,
1083                client_id,
1084                p,
1085            ),
1086            None => Strategy::cancel_all_orders(inner, instrument_id, order_side, client_id),
1087        }
1088        .map_err(to_pyruntime_err)
1089    }
1090
1091    #[pyo3(name = "close_position")]
1092    #[pyo3(signature = (position, client_id=None, tags=None, time_in_force=None, reduce_only=None, quote_quantity=None))]
1093    fn py_close_position(
1094        &mut self,
1095        position: &Position,
1096        client_id: Option<ClientId>,
1097        tags: Option<Vec<String>>,
1098        time_in_force: Option<TimeInForce>,
1099        reduce_only: Option<bool>,
1100        quote_quantity: Option<bool>,
1101    ) -> PyResult<()> {
1102        let tags = tags.map(|t| t.into_iter().map(|s| Ustr::from(&s)).collect());
1103        Strategy::close_position(
1104            self.inner_mut(),
1105            position,
1106            client_id,
1107            tags,
1108            time_in_force,
1109            reduce_only,
1110            quote_quantity,
1111        )
1112        .map_err(to_pyruntime_err)
1113    }
1114
1115    #[pyo3(name = "close_all_positions")]
1116    #[pyo3(signature = (instrument_id, position_side=None, client_id=None, tags=None, time_in_force=None, reduce_only=None, quote_quantity=None))]
1117    #[allow(clippy::too_many_arguments)]
1118    fn py_close_all_positions(
1119        &mut self,
1120        instrument_id: InstrumentId,
1121        position_side: Option<PositionSide>,
1122        client_id: Option<ClientId>,
1123        tags: Option<Vec<String>>,
1124        time_in_force: Option<TimeInForce>,
1125        reduce_only: Option<bool>,
1126        quote_quantity: Option<bool>,
1127    ) -> PyResult<()> {
1128        let tags = tags.map(|t| t.into_iter().map(|s| Ustr::from(&s)).collect());
1129        Strategy::close_all_positions(
1130            self.inner_mut(),
1131            instrument_id,
1132            position_side,
1133            client_id,
1134            tags,
1135            time_in_force,
1136            reduce_only,
1137            quote_quantity,
1138        )
1139        .map_err(to_pyruntime_err)
1140    }
1141
1142    #[pyo3(name = "query_account")]
1143    #[pyo3(signature = (account_id, client_id=None))]
1144    fn py_query_account(
1145        &mut self,
1146        account_id: AccountId,
1147        client_id: Option<ClientId>,
1148    ) -> PyResult<()> {
1149        Strategy::query_account(self.inner_mut(), account_id, client_id).map_err(to_pyruntime_err)
1150    }
1151
1152    #[pyo3(name = "query_order")]
1153    #[pyo3(signature = (order, client_id=None))]
1154    fn py_query_order(
1155        &mut self,
1156        py: Python<'_>,
1157        order: Py<PyAny>,
1158        client_id: Option<ClientId>,
1159    ) -> PyResult<()> {
1160        let order = pyobject_to_order_any(py, order)?;
1161        Strategy::query_order(self.inner_mut(), &order, client_id).map_err(to_pyruntime_err)
1162    }
1163
1164    #[pyo3(name = "on_start")]
1165    fn py_on_start(&mut self) -> PyResult<()> {
1166        self.inner_mut().dispatch_on_start()
1167    }
1168
1169    #[pyo3(name = "on_stop")]
1170    fn py_on_stop(&mut self) -> PyResult<()> {
1171        self.inner_mut().dispatch_on_stop()
1172    }
1173
1174    #[pyo3(name = "on_resume")]
1175    fn py_on_resume(&mut self) -> PyResult<()> {
1176        self.inner_mut().dispatch_on_resume()
1177    }
1178
1179    #[pyo3(name = "on_reset")]
1180    fn py_on_reset(&mut self) -> PyResult<()> {
1181        self.inner_mut().dispatch_on_reset()
1182    }
1183
1184    #[pyo3(name = "on_dispose")]
1185    fn py_on_dispose(&mut self) -> PyResult<()> {
1186        self.inner_mut().dispatch_on_dispose()
1187    }
1188
1189    #[pyo3(name = "on_degrade")]
1190    fn py_on_degrade(&mut self) -> PyResult<()> {
1191        self.inner_mut().dispatch_on_degrade()
1192    }
1193
1194    #[pyo3(name = "on_fault")]
1195    fn py_on_fault(&mut self) -> PyResult<()> {
1196        self.inner_mut().dispatch_on_fault()
1197    }
1198
1199    #[pyo3(name = "on_data")]
1200    fn py_on_data(&mut self, data: Py<PyAny>) -> PyResult<()> {
1201        self.inner_mut().dispatch_on_data(data)
1202    }
1203
1204    #[pyo3(name = "on_signal")]
1205    fn py_on_signal(&mut self, signal: &Signal) -> PyResult<()> {
1206        self.inner_mut().dispatch_on_signal(signal)
1207    }
1208
1209    #[pyo3(name = "on_instrument")]
1210    fn py_on_instrument(&mut self, instrument: Py<PyAny>) -> PyResult<()> {
1211        self.inner_mut().dispatch_on_instrument(instrument)
1212    }
1213
1214    #[pyo3(name = "on_quote")]
1215    fn py_on_quote(&mut self, quote: QuoteTick) -> PyResult<()> {
1216        self.inner_mut().dispatch_on_quote(quote)
1217    }
1218
1219    #[pyo3(name = "on_trade")]
1220    fn py_on_trade(&mut self, trade: TradeTick) -> PyResult<()> {
1221        self.inner_mut().dispatch_on_trade(trade)
1222    }
1223
1224    #[pyo3(name = "on_bar")]
1225    fn py_on_bar(&mut self, bar: Bar) -> PyResult<()> {
1226        self.inner_mut().dispatch_on_bar(bar)
1227    }
1228
1229    #[pyo3(name = "on_book_deltas")]
1230    fn py_on_book_deltas(&mut self, deltas: OrderBookDeltas) -> PyResult<()> {
1231        self.inner_mut().dispatch_on_book_deltas(deltas)
1232    }
1233
1234    #[pyo3(name = "on_book")]
1235    fn py_on_book(&mut self, book: &OrderBook) -> PyResult<()> {
1236        self.inner_mut().dispatch_on_book(book)
1237    }
1238
1239    #[pyo3(name = "on_mark_price")]
1240    fn py_on_mark_price(&mut self, mark_price: MarkPriceUpdate) -> PyResult<()> {
1241        self.inner_mut().dispatch_on_mark_price(mark_price)
1242    }
1243
1244    #[pyo3(name = "on_index_price")]
1245    fn py_on_index_price(&mut self, index_price: IndexPriceUpdate) -> PyResult<()> {
1246        self.inner_mut().dispatch_on_index_price(index_price)
1247    }
1248
1249    #[pyo3(name = "on_funding_rate")]
1250    fn py_on_funding_rate(&mut self, funding_rate: FundingRateUpdate) -> PyResult<()> {
1251        self.inner_mut().dispatch_on_funding_rate(funding_rate)
1252    }
1253
1254    #[pyo3(name = "on_instrument_status")]
1255    fn py_on_instrument_status(&mut self, status: InstrumentStatus) -> PyResult<()> {
1256        self.inner_mut().dispatch_on_instrument_status(status)
1257    }
1258
1259    #[pyo3(name = "on_instrument_close")]
1260    fn py_on_instrument_close(&mut self, close: InstrumentClose) -> PyResult<()> {
1261        self.inner_mut().dispatch_on_instrument_close(close)
1262    }
1263
1264    #[pyo3(name = "subscribe_data")]
1265    #[pyo3(signature = (data_type, client_id=None, params=None))]
1266    fn py_subscribe_data(
1267        &mut self,
1268        data_type: DataType,
1269        client_id: Option<ClientId>,
1270        params: Option<IndexMap<String, String>>,
1271    ) -> PyResult<()> {
1272        DataActor::subscribe_data(self.inner_mut(), data_type, client_id, params);
1273        Ok(())
1274    }
1275
1276    #[pyo3(name = "subscribe_instruments")]
1277    #[pyo3(signature = (venue, client_id=None, params=None))]
1278    fn py_subscribe_instruments(
1279        &mut self,
1280        venue: Venue,
1281        client_id: Option<ClientId>,
1282        params: Option<IndexMap<String, String>>,
1283    ) -> PyResult<()> {
1284        DataActor::subscribe_instruments(self.inner_mut(), venue, client_id, params);
1285        Ok(())
1286    }
1287
1288    #[pyo3(name = "subscribe_instrument")]
1289    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1290    fn py_subscribe_instrument(
1291        &mut self,
1292        instrument_id: InstrumentId,
1293        client_id: Option<ClientId>,
1294        params: Option<IndexMap<String, String>>,
1295    ) -> PyResult<()> {
1296        DataActor::subscribe_instrument(self.inner_mut(), instrument_id, client_id, params);
1297        Ok(())
1298    }
1299
1300    #[pyo3(name = "subscribe_book_deltas")]
1301    #[pyo3(signature = (instrument_id, book_type, depth=None, client_id=None, managed=false, params=None))]
1302    fn py_subscribe_book_deltas(
1303        &mut self,
1304        instrument_id: InstrumentId,
1305        book_type: BookType,
1306        depth: Option<usize>,
1307        client_id: Option<ClientId>,
1308        managed: bool,
1309        params: Option<IndexMap<String, String>>,
1310    ) -> PyResult<()> {
1311        let depth = depth.and_then(NonZeroUsize::new);
1312        DataActor::subscribe_book_deltas(
1313            self.inner_mut(),
1314            instrument_id,
1315            book_type,
1316            depth,
1317            client_id,
1318            managed,
1319            params,
1320        );
1321        Ok(())
1322    }
1323
1324    #[pyo3(name = "subscribe_book_at_interval")]
1325    #[pyo3(signature = (instrument_id, book_type, interval_ms, depth=None, client_id=None, params=None))]
1326    fn py_subscribe_book_at_interval(
1327        &mut self,
1328        instrument_id: InstrumentId,
1329        book_type: BookType,
1330        interval_ms: usize,
1331        depth: Option<usize>,
1332        client_id: Option<ClientId>,
1333        params: Option<IndexMap<String, String>>,
1334    ) -> PyResult<()> {
1335        let depth = depth.and_then(NonZeroUsize::new);
1336        let interval_ms = NonZeroUsize::new(interval_ms)
1337            .ok_or_else(|| to_pyvalue_err("interval_ms must be > 0"))?;
1338
1339        DataActor::subscribe_book_at_interval(
1340            self.inner_mut(),
1341            instrument_id,
1342            book_type,
1343            depth,
1344            interval_ms,
1345            client_id,
1346            params,
1347        );
1348        Ok(())
1349    }
1350
1351    #[pyo3(name = "subscribe_quotes")]
1352    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1353    fn py_subscribe_quotes(
1354        &mut self,
1355        instrument_id: InstrumentId,
1356        client_id: Option<ClientId>,
1357        params: Option<IndexMap<String, String>>,
1358    ) -> PyResult<()> {
1359        DataActor::subscribe_quotes(self.inner_mut(), instrument_id, client_id, params);
1360        Ok(())
1361    }
1362
1363    #[pyo3(name = "subscribe_trades")]
1364    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1365    fn py_subscribe_trades(
1366        &mut self,
1367        instrument_id: InstrumentId,
1368        client_id: Option<ClientId>,
1369        params: Option<IndexMap<String, String>>,
1370    ) -> PyResult<()> {
1371        DataActor::subscribe_trades(self.inner_mut(), instrument_id, client_id, params);
1372        Ok(())
1373    }
1374
1375    #[pyo3(name = "subscribe_bars")]
1376    #[pyo3(signature = (bar_type, client_id=None, params=None))]
1377    fn py_subscribe_bars(
1378        &mut self,
1379        bar_type: BarType,
1380        client_id: Option<ClientId>,
1381        params: Option<IndexMap<String, String>>,
1382    ) -> PyResult<()> {
1383        DataActor::subscribe_bars(self.inner_mut(), bar_type, client_id, params);
1384        Ok(())
1385    }
1386
1387    #[pyo3(name = "subscribe_mark_prices")]
1388    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1389    fn py_subscribe_mark_prices(
1390        &mut self,
1391        instrument_id: InstrumentId,
1392        client_id: Option<ClientId>,
1393        params: Option<IndexMap<String, String>>,
1394    ) -> PyResult<()> {
1395        DataActor::subscribe_mark_prices(self.inner_mut(), instrument_id, client_id, params);
1396        Ok(())
1397    }
1398
1399    #[pyo3(name = "subscribe_index_prices")]
1400    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1401    fn py_subscribe_index_prices(
1402        &mut self,
1403        instrument_id: InstrumentId,
1404        client_id: Option<ClientId>,
1405        params: Option<IndexMap<String, String>>,
1406    ) -> PyResult<()> {
1407        DataActor::subscribe_index_prices(self.inner_mut(), instrument_id, client_id, params);
1408        Ok(())
1409    }
1410
1411    #[pyo3(name = "subscribe_instrument_status")]
1412    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1413    fn py_subscribe_instrument_status(
1414        &mut self,
1415        instrument_id: InstrumentId,
1416        client_id: Option<ClientId>,
1417        params: Option<IndexMap<String, String>>,
1418    ) -> PyResult<()> {
1419        DataActor::subscribe_instrument_status(self.inner_mut(), instrument_id, client_id, params);
1420        Ok(())
1421    }
1422
1423    #[pyo3(name = "subscribe_instrument_close")]
1424    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1425    fn py_subscribe_instrument_close(
1426        &mut self,
1427        instrument_id: InstrumentId,
1428        client_id: Option<ClientId>,
1429        params: Option<IndexMap<String, String>>,
1430    ) -> PyResult<()> {
1431        DataActor::subscribe_instrument_close(self.inner_mut(), instrument_id, client_id, params);
1432        Ok(())
1433    }
1434
1435    #[pyo3(name = "request_data")]
1436    #[pyo3(signature = (data_type, client_id, start=None, end=None, limit=None, params=None))]
1437    fn py_request_data(
1438        &mut self,
1439        data_type: DataType,
1440        client_id: ClientId,
1441        start: Option<u64>,
1442        end: Option<u64>,
1443        limit: Option<usize>,
1444        params: Option<IndexMap<String, String>>,
1445    ) -> PyResult<String> {
1446        let limit = limit.and_then(NonZeroUsize::new);
1447        let start = start.map(|ts| UnixNanos::from(ts).to_datetime_utc());
1448        let end = end.map(|ts| UnixNanos::from(ts).to_datetime_utc());
1449
1450        let request_id = DataActor::request_data(
1451            self.inner_mut(),
1452            data_type,
1453            client_id,
1454            start,
1455            end,
1456            limit,
1457            params,
1458        )
1459        .map_err(to_pyvalue_err)?;
1460        Ok(request_id.to_string())
1461    }
1462
1463    #[pyo3(name = "request_instrument")]
1464    #[pyo3(signature = (instrument_id, start=None, end=None, client_id=None, params=None))]
1465    fn py_request_instrument(
1466        &mut self,
1467        instrument_id: InstrumentId,
1468        start: Option<u64>,
1469        end: Option<u64>,
1470        client_id: Option<ClientId>,
1471        params: Option<IndexMap<String, String>>,
1472    ) -> PyResult<String> {
1473        let start = start.map(|ts| UnixNanos::from(ts).to_datetime_utc());
1474        let end = end.map(|ts| UnixNanos::from(ts).to_datetime_utc());
1475
1476        let request_id = DataActor::request_instrument(
1477            self.inner_mut(),
1478            instrument_id,
1479            start,
1480            end,
1481            client_id,
1482            params,
1483        )
1484        .map_err(to_pyvalue_err)?;
1485        Ok(request_id.to_string())
1486    }
1487
1488    #[pyo3(name = "request_instruments")]
1489    #[pyo3(signature = (venue=None, start=None, end=None, client_id=None, params=None))]
1490    fn py_request_instruments(
1491        &mut self,
1492        venue: Option<Venue>,
1493        start: Option<u64>,
1494        end: Option<u64>,
1495        client_id: Option<ClientId>,
1496        params: Option<IndexMap<String, String>>,
1497    ) -> PyResult<String> {
1498        let start = start.map(|ts| UnixNanos::from(ts).to_datetime_utc());
1499        let end = end.map(|ts| UnixNanos::from(ts).to_datetime_utc());
1500
1501        let request_id =
1502            DataActor::request_instruments(self.inner_mut(), venue, start, end, client_id, params)
1503                .map_err(to_pyvalue_err)?;
1504        Ok(request_id.to_string())
1505    }
1506
1507    #[pyo3(name = "request_book_snapshot")]
1508    #[pyo3(signature = (instrument_id, depth=None, client_id=None, params=None))]
1509    fn py_request_book_snapshot(
1510        &mut self,
1511        instrument_id: InstrumentId,
1512        depth: Option<usize>,
1513        client_id: Option<ClientId>,
1514        params: Option<IndexMap<String, String>>,
1515    ) -> PyResult<String> {
1516        let depth = depth.and_then(NonZeroUsize::new);
1517
1518        let request_id = DataActor::request_book_snapshot(
1519            self.inner_mut(),
1520            instrument_id,
1521            depth,
1522            client_id,
1523            params,
1524        )
1525        .map_err(to_pyvalue_err)?;
1526        Ok(request_id.to_string())
1527    }
1528
1529    #[pyo3(name = "request_quotes")]
1530    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None))]
1531    fn py_request_quotes(
1532        &mut self,
1533        instrument_id: InstrumentId,
1534        start: Option<u64>,
1535        end: Option<u64>,
1536        limit: Option<usize>,
1537        client_id: Option<ClientId>,
1538        params: Option<IndexMap<String, String>>,
1539    ) -> PyResult<String> {
1540        let limit = limit.and_then(NonZeroUsize::new);
1541        let start = start.map(|ts| UnixNanos::from(ts).to_datetime_utc());
1542        let end = end.map(|ts| UnixNanos::from(ts).to_datetime_utc());
1543
1544        let request_id = DataActor::request_quotes(
1545            self.inner_mut(),
1546            instrument_id,
1547            start,
1548            end,
1549            limit,
1550            client_id,
1551            params,
1552        )
1553        .map_err(to_pyvalue_err)?;
1554        Ok(request_id.to_string())
1555    }
1556
1557    #[pyo3(name = "request_trades")]
1558    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None))]
1559    fn py_request_trades(
1560        &mut self,
1561        instrument_id: InstrumentId,
1562        start: Option<u64>,
1563        end: Option<u64>,
1564        limit: Option<usize>,
1565        client_id: Option<ClientId>,
1566        params: Option<IndexMap<String, String>>,
1567    ) -> PyResult<String> {
1568        let limit = limit.and_then(NonZeroUsize::new);
1569        let start = start.map(|ts| UnixNanos::from(ts).to_datetime_utc());
1570        let end = end.map(|ts| UnixNanos::from(ts).to_datetime_utc());
1571
1572        let request_id = DataActor::request_trades(
1573            self.inner_mut(),
1574            instrument_id,
1575            start,
1576            end,
1577            limit,
1578            client_id,
1579            params,
1580        )
1581        .map_err(to_pyvalue_err)?;
1582        Ok(request_id.to_string())
1583    }
1584
1585    #[pyo3(name = "request_bars")]
1586    #[pyo3(signature = (bar_type, start=None, end=None, limit=None, client_id=None, params=None))]
1587    fn py_request_bars(
1588        &mut self,
1589        bar_type: BarType,
1590        start: Option<u64>,
1591        end: Option<u64>,
1592        limit: Option<usize>,
1593        client_id: Option<ClientId>,
1594        params: Option<IndexMap<String, String>>,
1595    ) -> PyResult<String> {
1596        let limit = limit.and_then(NonZeroUsize::new);
1597        let start = start.map(|ts| UnixNanos::from(ts).to_datetime_utc());
1598        let end = end.map(|ts| UnixNanos::from(ts).to_datetime_utc());
1599
1600        let request_id = DataActor::request_bars(
1601            self.inner_mut(),
1602            bar_type,
1603            start,
1604            end,
1605            limit,
1606            client_id,
1607            params,
1608        )
1609        .map_err(to_pyvalue_err)?;
1610        Ok(request_id.to_string())
1611    }
1612
1613    #[pyo3(name = "unsubscribe_data")]
1614    #[pyo3(signature = (data_type, client_id=None, params=None))]
1615    fn py_unsubscribe_data(
1616        &mut self,
1617        data_type: DataType,
1618        client_id: Option<ClientId>,
1619        params: Option<IndexMap<String, String>>,
1620    ) -> PyResult<()> {
1621        DataActor::unsubscribe_data(self.inner_mut(), data_type, client_id, params);
1622        Ok(())
1623    }
1624
1625    #[pyo3(name = "unsubscribe_instruments")]
1626    #[pyo3(signature = (venue, client_id=None, params=None))]
1627    fn py_unsubscribe_instruments(
1628        &mut self,
1629        venue: Venue,
1630        client_id: Option<ClientId>,
1631        params: Option<IndexMap<String, String>>,
1632    ) -> PyResult<()> {
1633        DataActor::unsubscribe_instruments(self.inner_mut(), venue, client_id, params);
1634        Ok(())
1635    }
1636
1637    #[pyo3(name = "unsubscribe_instrument")]
1638    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1639    fn py_unsubscribe_instrument(
1640        &mut self,
1641        instrument_id: InstrumentId,
1642        client_id: Option<ClientId>,
1643        params: Option<IndexMap<String, String>>,
1644    ) -> PyResult<()> {
1645        DataActor::unsubscribe_instrument(self.inner_mut(), instrument_id, client_id, params);
1646        Ok(())
1647    }
1648
1649    #[pyo3(name = "unsubscribe_book_deltas")]
1650    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1651    fn py_unsubscribe_book_deltas(
1652        &mut self,
1653        instrument_id: InstrumentId,
1654        client_id: Option<ClientId>,
1655        params: Option<IndexMap<String, String>>,
1656    ) -> PyResult<()> {
1657        DataActor::unsubscribe_book_deltas(self.inner_mut(), instrument_id, client_id, params);
1658        Ok(())
1659    }
1660
1661    #[pyo3(name = "unsubscribe_book_at_interval")]
1662    #[pyo3(signature = (instrument_id, interval_ms, client_id=None, params=None))]
1663    fn py_unsubscribe_book_at_interval(
1664        &mut self,
1665        instrument_id: InstrumentId,
1666        interval_ms: usize,
1667        client_id: Option<ClientId>,
1668        params: Option<IndexMap<String, String>>,
1669    ) -> PyResult<()> {
1670        let interval_ms = NonZeroUsize::new(interval_ms)
1671            .ok_or_else(|| to_pyvalue_err("interval_ms must be > 0"))?;
1672
1673        DataActor::unsubscribe_book_at_interval(
1674            self.inner_mut(),
1675            instrument_id,
1676            interval_ms,
1677            client_id,
1678            params,
1679        );
1680        Ok(())
1681    }
1682
1683    #[pyo3(name = "unsubscribe_quotes")]
1684    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1685    fn py_unsubscribe_quotes(
1686        &mut self,
1687        instrument_id: InstrumentId,
1688        client_id: Option<ClientId>,
1689        params: Option<IndexMap<String, String>>,
1690    ) -> PyResult<()> {
1691        DataActor::unsubscribe_quotes(self.inner_mut(), instrument_id, client_id, params);
1692        Ok(())
1693    }
1694
1695    #[pyo3(name = "unsubscribe_trades")]
1696    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1697    fn py_unsubscribe_trades(
1698        &mut self,
1699        instrument_id: InstrumentId,
1700        client_id: Option<ClientId>,
1701        params: Option<IndexMap<String, String>>,
1702    ) -> PyResult<()> {
1703        DataActor::unsubscribe_trades(self.inner_mut(), instrument_id, client_id, params);
1704        Ok(())
1705    }
1706
1707    #[pyo3(name = "unsubscribe_bars")]
1708    #[pyo3(signature = (bar_type, client_id=None, params=None))]
1709    fn py_unsubscribe_bars(
1710        &mut self,
1711        bar_type: BarType,
1712        client_id: Option<ClientId>,
1713        params: Option<IndexMap<String, String>>,
1714    ) -> PyResult<()> {
1715        DataActor::unsubscribe_bars(self.inner_mut(), bar_type, client_id, params);
1716        Ok(())
1717    }
1718
1719    #[pyo3(name = "unsubscribe_mark_prices")]
1720    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1721    fn py_unsubscribe_mark_prices(
1722        &mut self,
1723        instrument_id: InstrumentId,
1724        client_id: Option<ClientId>,
1725        params: Option<IndexMap<String, String>>,
1726    ) -> PyResult<()> {
1727        DataActor::unsubscribe_mark_prices(self.inner_mut(), instrument_id, client_id, params);
1728        Ok(())
1729    }
1730
1731    #[pyo3(name = "unsubscribe_index_prices")]
1732    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1733    fn py_unsubscribe_index_prices(
1734        &mut self,
1735        instrument_id: InstrumentId,
1736        client_id: Option<ClientId>,
1737        params: Option<IndexMap<String, String>>,
1738    ) -> PyResult<()> {
1739        DataActor::unsubscribe_index_prices(self.inner_mut(), instrument_id, client_id, params);
1740        Ok(())
1741    }
1742
1743    #[pyo3(name = "unsubscribe_instrument_status")]
1744    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1745    fn py_unsubscribe_instrument_status(
1746        &mut self,
1747        instrument_id: InstrumentId,
1748        client_id: Option<ClientId>,
1749        params: Option<IndexMap<String, String>>,
1750    ) -> PyResult<()> {
1751        DataActor::unsubscribe_instrument_status(
1752            self.inner_mut(),
1753            instrument_id,
1754            client_id,
1755            params,
1756        );
1757        Ok(())
1758    }
1759
1760    #[pyo3(name = "unsubscribe_instrument_close")]
1761    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1762    fn py_unsubscribe_instrument_close(
1763        &mut self,
1764        instrument_id: InstrumentId,
1765        client_id: Option<ClientId>,
1766        params: Option<IndexMap<String, String>>,
1767    ) -> PyResult<()> {
1768        DataActor::unsubscribe_instrument_close(self.inner_mut(), instrument_id, client_id, params);
1769        Ok(())
1770    }
1771}