nautilus_trading/python/
strategy.rs

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