1use std::{
19 any::Any,
20 cell::{RefCell, UnsafeCell},
21 collections::HashMap,
22 fmt::Debug,
23 num::NonZeroUsize,
24 ops::{Deref, DerefMut},
25 rc::Rc,
26};
27
28use indexmap::IndexMap;
29use jiff::Timestamp;
30use nautilus_common::{
31 actor::{
32 Actor, DataActor, DataActorNative,
33 data_actor::DataActorCore,
34 registry::{try_get_actor_unchecked, with_actor_registry},
35 },
36 cache::Cache,
37 clock::Clock,
38 component::{Component, with_component_registry},
39 enums::ComponentState,
40 messages::{
41 data::{
42 DataResponseEnvelope, ensure_routable_bar_types, ensure_routable_instrument_ids,
43 ensure_subscription_routes, quote_routing,
44 },
45 system::{QueueStateChanged, SocketStateChanged},
46 },
47 msgbus::TypedHandler,
48 python::{
49 actor::{extract_bar_types, extract_instrument_ids},
50 cache::PyCache,
51 clock::PyClock,
52 config_error_to_pyvalue_err,
53 indicators::{registered_python_indicators, wrap_python_indicator},
54 logging::{PyLogger, format_exception},
55 order_factory::PyOrderFactory,
56 response::PyDataResponse,
57 wrappers::{get_python_message_bus, retain_python_wrapper},
58 },
59 runner::SystemChannel,
60 signal::Signal,
61 timer::{TimeEvent, TimeEventCallback},
62};
63use nautilus_core::{
64 Params, UnixNanos,
65 correctness::{CorrectnessResult, CorrectnessResultExt, FAILED},
66 from_pydict,
67 python::{to_pyruntime_err, to_pyvalue_err, upgrade_py_weakref},
68};
69use nautilus_model::{
70 data::{
71 Bar, BarType, CustomData, DataType, FundingRateUpdate, IndexPriceUpdate, InstrumentStatus,
72 MarkPriceUpdate, OrderBookDelta, OrderBookDeltas, OrderBookDepth, QuoteTick, TradeTick,
73 close::InstrumentClose,
74 option_chain::{OptionChainSlice, OptionGreeks},
75 },
76 enums::{BookType, OmsType, OrderSide, PositionSide, TimeInForce},
77 events::{
78 OrderAccepted, OrderCancelRejected, OrderCanceled, OrderDenied, OrderEmulated,
79 OrderEventAny, OrderExpired, OrderFillVoided, OrderFilled, OrderInitialized,
80 OrderModifyRejected, OrderPendingCancel, OrderPendingUpdate, OrderRejected, OrderReleased,
81 OrderSubmitted, OrderTriggered, OrderUpdated, PositionChanged, PositionClosed,
82 PositionEvent, PositionOpened,
83 },
84 identifiers::{
85 AccountId, ActorId, ClientId, ClientOrderId, InstrumentId, OptionSeriesId, PositionId,
86 StrategyId, TraderId, UNASSIGNED_ORDER_ID_TAG, Venue, normalize_order_id_tag,
87 },
88 instruments::{InstrumentAny, SyntheticInstrument},
89 orderbook::OrderBook,
90 orders::{Order, OrderAny},
91 position::Position,
92 python::{
93 data::option_chain::PyStrikeRange, events::order::order_event_to_pyobject,
94 instruments::instrument_any_to_pyobject, orders::pyobject_to_order_any,
95 },
96 types::{Price, Quantity},
97};
98use nautilus_portfolio::{portfolio::Portfolio, python::PyPortfolio};
99use pyo3::{
100 IntoPyObjectExt,
101 prelude::*,
102 types::{PyBytes, PyDict, PyList, PySet, PyWeakrefReference},
103};
104use ustr::Ustr;
105
106use crate::strategy::{
107 BatchModifyOrder, ImportableStrategyConfig, Strategy, StrategyConfig, StrategyCore,
108 StrategyNative, route_time_event,
109};
110
111#[pyo3::pymethods]
112#[pyo3_stub_gen::derive::gen_stub_pymethods]
113impl StrategyConfig {
114 #[new]
116 #[pyo3(signature = (
117 strategy_id=None,
118 order_id_tag=None,
119 oms_type=None,
120 external_order_instrument_ids=None,
121 manage_contingent_orders=false,
122 manage_gtd_expiry=false,
123 manage_stop=false,
124 market_exit_interval_ms=100,
125 market_exit_max_attempts=100,
126 market_exit_time_in_force=TimeInForce::Gtc,
127 market_exit_reduce_only=true,
128 use_uuid_client_order_ids=false,
129 use_hyphens_in_client_order_ids=true,
130 log_events=true,
131 log_commands=true,
132 log_rejected_due_post_only_as_warning=true,
133 **_kwargs
134 ))]
135 #[expect(
136 clippy::fn_params_excessive_bools,
137 clippy::too_many_arguments,
138 reason = "constructor mirrors the existing Python keyword API"
139 )]
140 fn py_new(
141 strategy_id: Option<StrategyId>,
142 order_id_tag: Option<String>,
143 oms_type: Option<OmsType>,
144 external_order_instrument_ids: Option<Vec<InstrumentId>>,
145 manage_contingent_orders: bool,
146 manage_gtd_expiry: bool,
147 manage_stop: bool,
148 market_exit_interval_ms: u64,
149 market_exit_max_attempts: u64,
150 market_exit_time_in_force: TimeInForce,
151 market_exit_reduce_only: bool,
152 use_uuid_client_order_ids: bool,
153 use_hyphens_in_client_order_ids: bool,
154 log_events: bool,
155 log_commands: bool,
156 log_rejected_due_post_only_as_warning: bool,
157 _kwargs: Option<&Bound<'_, PyDict>>,
158 ) -> PyResult<Self> {
159 let config = Self {
160 strategy_id,
161 order_id_tag,
162 use_uuid_client_order_ids,
163 use_hyphens_in_client_order_ids,
164 oms_type,
165 external_order_instrument_ids,
166 manage_contingent_orders,
167 manage_gtd_expiry,
168 manage_stop,
169 market_exit_interval_ms,
170 market_exit_max_attempts,
171 market_exit_time_in_force,
172 market_exit_reduce_only,
173 log_events,
174 log_commands,
175 log_rejected_due_post_only_as_warning,
176 };
177 config.validate().map_err(config_error_to_pyvalue_err)?;
178 Ok(config)
179 }
180
181 #[getter]
182 fn strategy_id(&self) -> Option<StrategyId> {
183 self.strategy_id
184 }
185
186 #[getter]
187 fn order_id_tag(&self) -> Option<&String> {
188 self.order_id_tag.as_ref()
189 }
190
191 #[getter]
192 fn oms_type(&self) -> Option<OmsType> {
193 self.oms_type
194 }
195
196 #[getter]
197 fn external_order_instrument_ids(&self) -> Option<Vec<InstrumentId>> {
198 self.external_order_instrument_ids.clone()
199 }
200
201 #[getter]
202 fn manage_contingent_orders(&self) -> bool {
203 self.manage_contingent_orders
204 }
205
206 #[getter]
207 fn manage_gtd_expiry(&self) -> bool {
208 self.manage_gtd_expiry
209 }
210
211 #[getter]
212 fn manage_stop(&self) -> bool {
213 self.manage_stop
214 }
215
216 #[getter]
217 fn market_exit_interval_ms(&self) -> u64 {
218 self.market_exit_interval_ms
219 }
220
221 #[getter]
222 fn market_exit_max_attempts(&self) -> u64 {
223 self.market_exit_max_attempts
224 }
225
226 #[getter]
227 fn market_exit_time_in_force(&self) -> TimeInForce {
228 self.market_exit_time_in_force
229 }
230
231 #[getter]
232 fn market_exit_reduce_only(&self) -> bool {
233 self.market_exit_reduce_only
234 }
235
236 #[getter]
237 fn use_uuid_client_order_ids(&self) -> bool {
238 self.use_uuid_client_order_ids
239 }
240
241 #[getter]
242 fn use_hyphens_in_client_order_ids(&self) -> bool {
243 self.use_hyphens_in_client_order_ids
244 }
245
246 #[getter]
247 fn log_events(&self) -> bool {
248 self.log_events
249 }
250
251 #[getter]
252 fn log_commands(&self) -> bool {
253 self.log_commands
254 }
255
256 #[getter]
257 fn log_rejected_due_post_only_as_warning(&self) -> bool {
258 self.log_rejected_due_post_only_as_warning
259 }
260}
261
262#[pyo3::pymethods]
263#[pyo3_stub_gen::derive::gen_stub_pymethods]
264impl ImportableStrategyConfig {
265 #[new]
267 #[expect(clippy::needless_pass_by_value)]
268 fn py_new(strategy_path: String, config_path: String, config: Py<PyDict>) -> PyResult<Self> {
269 let json_config = Python::attach(|py| -> PyResult<HashMap<String, serde_json::Value>> {
270 let kwargs = PyDict::new(py);
271 kwargs.set_item("default", py.eval(pyo3::ffi::c_str!("str"), None, None)?)?;
272 let json_str: String = PyModule::import(py, "json")?
273 .call_method("dumps", (config.bind(py),), Some(&kwargs))?
274 .extract()?;
275
276 let json_value: serde_json::Value =
277 serde_json::from_str(&json_str).map_err(to_pyvalue_err)?;
278
279 if let serde_json::Value::Object(map) = json_value {
280 Ok(map.into_iter().collect())
281 } else {
282 Err(to_pyvalue_err("Config must be a dictionary"))
283 }
284 })?;
285
286 Ok(Self {
287 strategy_path,
288 config_path,
289 config: json_config,
290 })
291 }
292
293 #[getter]
294 fn strategy_path(&self) -> &String {
295 &self.strategy_path
296 }
297
298 #[getter]
299 fn config_path(&self) -> &String {
300 &self.config_path
301 }
302
303 #[getter]
304 fn config(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
305 let py_dict = PyDict::new(py);
306
307 for (key, value) in &self.config {
308 let json_str = serde_json::to_string(value).map_err(to_pyvalue_err)?;
309 let py_value = PyModule::import(py, "json")?.call_method("loads", (json_str,), None)?;
310 py_dict.set_item(key, py_value)?;
311 }
312 Ok(py_dict.unbind())
313 }
314}
315
316pub struct PyStrategyInner {
318 core: StrategyCore,
319 py_self: Option<Py<PyWeakrefReference>>,
320 config: Option<Py<PyAny>>,
321 clock: PyClock,
322 logger: PyLogger,
323}
324
325impl Debug for PyStrategyInner {
326 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
327 f.debug_struct(stringify!(PyStrategyInner))
328 .field("core", &self.core)
329 .field(
330 "py_self",
331 &self.py_self.as_ref().map(|_| "<Py<PyWeakrefReference>>"),
332 )
333 .field("config", &self.config.as_ref().map(|_| "<Py<PyAny>>"))
334 .field("clock", &self.clock)
335 .field("logger", &self.logger)
336 .finish()
337 }
338}
339
340#[expect(
341 clippy::needless_pass_by_ref_mut,
342 reason = "dispatch methods share receiver shape with mutable DataActor hooks"
343)]
344impl PyStrategyInner {
345 fn dispatch_on_start(&self) -> PyResult<()> {
346 if let Some(py_self) = self.python_instance()? {
347 Python::attach(|py| py_self.call_method0(py, "on_start"))?;
348 }
349 Ok(())
350 }
351
352 fn dispatch_on_stop(&self) -> PyResult<()> {
353 if let Some(py_self) = self.python_instance()? {
354 Python::attach(|py| py_self.call_method0(py, "on_stop"))?;
355 }
356 Ok(())
357 }
358
359 fn dispatch_on_resume(&self) -> PyResult<()> {
360 if let Some(py_self) = self.python_instance()? {
361 Python::attach(|py| py_self.call_method0(py, "on_resume"))?;
362 }
363 Ok(())
364 }
365
366 fn dispatch_on_reset(&self) -> PyResult<()> {
367 if let Some(py_self) = self.python_instance()? {
368 Python::attach(|py| py_self.call_method0(py, "on_reset"))?;
369 }
370 Ok(())
371 }
372
373 fn dispatch_on_dispose(&self) -> PyResult<()> {
374 if let Some(py_self) = self.python_instance()? {
375 Python::attach(|py| py_self.call_method0(py, "on_dispose"))?;
376 }
377 Ok(())
378 }
379
380 fn dispatch_on_degrade(&self) -> PyResult<()> {
381 if let Some(py_self) = self.python_instance()? {
382 Python::attach(|py| py_self.call_method0(py, "on_degrade"))?;
383 }
384 Ok(())
385 }
386
387 fn dispatch_on_fault(&self) -> PyResult<()> {
388 if let Some(py_self) = self.python_instance()? {
389 Python::attach(|py| py_self.call_method0(py, "on_fault"))?;
390 }
391 Ok(())
392 }
393
394 fn dispatch_on_save(&self) -> PyResult<IndexMap<String, Vec<u8>>> {
395 if let Some(py_self) = self.python_instance()? {
396 Python::attach(|py| {
397 let py_state = py_self.call_method0(py, "on_save")?;
398 let py_state: &Bound<'_, PyDict> = py_state.cast_bound::<PyDict>(py)?;
399 pydict_to_state(py_state)
400 })
401 } else {
402 Ok(IndexMap::new())
403 }
404 }
405
406 fn dispatch_on_load(&self, state: &IndexMap<String, Vec<u8>>) -> PyResult<()> {
407 if let Some(py_self) = self.python_instance()? {
408 Python::attach(|py| -> PyResult<()> {
409 let py_state = state_to_pydict(py, state)?;
410 py_self.call_method1(py, "on_load", (py_state,))?;
411 Ok(())
412 })?;
413 }
414 Ok(())
415 }
416
417 fn dispatch_on_market_exit(&self) -> PyResult<()> {
418 if let Some(py_self) = self.python_instance()? {
419 Python::attach(|py| py_self.call_method0(py, "on_market_exit"))?;
420 }
421 Ok(())
422 }
423
424 fn dispatch_post_market_exit(&self) -> PyResult<()> {
425 if let Some(py_self) = self.python_instance()? {
426 Python::attach(|py| py_self.call_method0(py, "post_market_exit"))?;
427 }
428 Ok(())
429 }
430
431 fn dispatch_on_time_event(&self, event: &TimeEvent) -> PyResult<()> {
432 if let Some(py_self) = self.python_instance()? {
433 Python::attach(|py| {
434 py_self.call_method1(py, "on_time_event", (event.clone().into_py_any(py)?,))
435 })?;
436 }
437 Ok(())
438 }
439
440 fn dispatch_on_order_initialized(&self, event: OrderInitialized) -> PyResult<()> {
441 if let Some(py_self) = self.python_instance()? {
442 Python::attach(|py| {
443 py_self.call_method1(py, "on_order_initialized", (event.into_py_any(py)?,))
444 })?;
445 }
446 Ok(())
447 }
448
449 fn dispatch_on_order_event(&self, event: OrderEventAny) -> PyResult<()> {
450 if let Some(py_self) = self.python_instance()? {
451 Python::attach(|py| {
452 let py_event = order_event_to_pyobject(py, event)?;
453 py_self.call_method1(py, "on_order_event", (py_event,))
454 })?;
455 }
456 Ok(())
457 }
458
459 fn dispatch_on_order_denied(&self, event: OrderDenied) -> PyResult<()> {
460 if let Some(py_self) = self.python_instance()? {
461 Python::attach(|py| {
462 py_self.call_method1(py, "on_order_denied", (event.into_py_any(py)?,))
463 })?;
464 }
465 Ok(())
466 }
467
468 fn dispatch_on_order_emulated(&self, event: OrderEmulated) -> PyResult<()> {
469 if let Some(py_self) = self.python_instance()? {
470 Python::attach(|py| {
471 py_self.call_method1(py, "on_order_emulated", (event.into_py_any(py)?,))
472 })?;
473 }
474 Ok(())
475 }
476
477 fn dispatch_on_order_released(&self, event: OrderReleased) -> PyResult<()> {
478 if let Some(py_self) = self.python_instance()? {
479 Python::attach(|py| {
480 py_self.call_method1(py, "on_order_released", (event.into_py_any(py)?,))
481 })?;
482 }
483 Ok(())
484 }
485
486 fn dispatch_on_order_submitted(&self, event: OrderSubmitted) -> PyResult<()> {
487 if let Some(py_self) = self.python_instance()? {
488 Python::attach(|py| {
489 py_self.call_method1(py, "on_order_submitted", (event.into_py_any(py)?,))
490 })?;
491 }
492 Ok(())
493 }
494
495 fn dispatch_on_order_rejected(&self, event: OrderRejected) -> PyResult<()> {
496 if let Some(py_self) = self.python_instance()? {
497 Python::attach(|py| {
498 py_self.call_method1(py, "on_order_rejected", (event.into_py_any(py)?,))
499 })?;
500 }
501 Ok(())
502 }
503
504 fn dispatch_on_order_accepted(&self, event: OrderAccepted) -> PyResult<()> {
505 if let Some(py_self) = self.python_instance()? {
506 Python::attach(|py| {
507 py_self.call_method1(py, "on_order_accepted", (event.into_py_any(py)?,))
508 })?;
509 }
510 Ok(())
511 }
512
513 fn dispatch_on_order_expired(&self, event: OrderExpired) -> PyResult<()> {
514 if let Some(py_self) = self.python_instance()? {
515 Python::attach(|py| {
516 py_self.call_method1(py, "on_order_expired", (event.into_py_any(py)?,))
517 })?;
518 }
519 Ok(())
520 }
521
522 fn dispatch_on_order_triggered(&self, event: OrderTriggered) -> PyResult<()> {
523 if let Some(py_self) = self.python_instance()? {
524 Python::attach(|py| {
525 py_self.call_method1(py, "on_order_triggered", (event.into_py_any(py)?,))
526 })?;
527 }
528 Ok(())
529 }
530
531 fn dispatch_on_order_pending_update(&self, event: OrderPendingUpdate) -> PyResult<()> {
532 if let Some(py_self) = self.python_instance()? {
533 Python::attach(|py| {
534 py_self.call_method1(py, "on_order_pending_update", (event.into_py_any(py)?,))
535 })?;
536 }
537 Ok(())
538 }
539
540 fn dispatch_on_order_pending_cancel(&self, event: OrderPendingCancel) -> PyResult<()> {
541 if let Some(py_self) = self.python_instance()? {
542 Python::attach(|py| {
543 py_self.call_method1(py, "on_order_pending_cancel", (event.into_py_any(py)?,))
544 })?;
545 }
546 Ok(())
547 }
548
549 fn dispatch_on_order_modify_rejected(&self, event: OrderModifyRejected) -> PyResult<()> {
550 if let Some(py_self) = self.python_instance()? {
551 Python::attach(|py| {
552 py_self.call_method1(py, "on_order_modify_rejected", (event.into_py_any(py)?,))
553 })?;
554 }
555 Ok(())
556 }
557
558 fn dispatch_on_order_cancel_rejected(&self, event: OrderCancelRejected) -> PyResult<()> {
559 if let Some(py_self) = self.python_instance()? {
560 Python::attach(|py| {
561 py_self.call_method1(py, "on_order_cancel_rejected", (event.into_py_any(py)?,))
562 })?;
563 }
564 Ok(())
565 }
566
567 fn dispatch_on_order_updated(&self, event: &OrderUpdated) -> PyResult<()> {
568 if let Some(py_self) = self.python_instance()? {
569 Python::attach(|py| {
570 py_self.call_method1(py, "on_order_updated", ((*event).into_py_any(py)?,))
571 })?;
572 }
573 Ok(())
574 }
575
576 fn dispatch_on_order_canceled(&self, event: OrderCanceled) -> PyResult<()> {
577 if let Some(py_self) = self.python_instance()? {
578 Python::attach(|py| {
579 py_self.call_method1(py, "on_order_canceled", (event.into_py_any(py)?,))
580 })?;
581 }
582 Ok(())
583 }
584
585 fn dispatch_on_order_filled(&self, event: &OrderFilled) -> PyResult<()> {
586 if let Some(py_self) = self.python_instance()? {
587 Python::attach(|py| {
588 py_self.call_method1(py, "on_order_filled", (event.clone().into_py_any(py)?,))
589 })?;
590 }
591 Ok(())
592 }
593
594 fn dispatch_on_order_fill_voided(&self, event: &OrderFillVoided) -> PyResult<()> {
595 if let Some(py_self) = self.python_instance()? {
596 Python::attach(|py| {
597 py_self.call_method1(
598 py,
599 "on_order_fill_voided",
600 (event.clone().into_py_any(py)?,),
601 )
602 })?;
603 }
604 Ok(())
605 }
606
607 fn dispatch_on_position_opened(&self, event: PositionOpened) -> PyResult<()> {
608 if let Some(py_self) = self.python_instance()? {
609 Python::attach(|py| {
610 py_self.call_method1(py, "on_position_opened", (event.into_py_any(py)?,))
611 })?;
612 }
613 Ok(())
614 }
615
616 fn dispatch_on_position_event(&self, event: PositionEvent) -> PyResult<()> {
617 if let Some(py_self) = self.python_instance()? {
618 Python::attach(|py| {
619 let py_event = match event {
620 PositionEvent::PositionOpened(event) => event.into_py_any(py)?,
621 PositionEvent::PositionChanged(event) => event.into_py_any(py)?,
622 PositionEvent::PositionClosed(event) => event.into_py_any(py)?,
623 PositionEvent::PositionAdjusted(event) => event.into_py_any(py)?,
624 };
625 py_self.call_method1(py, "on_position_event", (py_event,))
626 })?;
627 }
628 Ok(())
629 }
630
631 fn dispatch_on_position_changed(&self, event: PositionChanged) -> PyResult<()> {
632 if let Some(py_self) = self.python_instance()? {
633 Python::attach(|py| {
634 py_self.call_method1(py, "on_position_changed", (event.into_py_any(py)?,))
635 })?;
636 }
637 Ok(())
638 }
639
640 fn dispatch_on_position_closed(&self, event: PositionClosed) -> PyResult<()> {
641 if let Some(py_self) = self.python_instance()? {
642 Python::attach(|py| {
643 py_self.call_method1(py, "on_position_closed", (event.into_py_any(py)?,))
644 })?;
645 }
646 Ok(())
647 }
648
649 fn dispatch_on_data(&mut self, data: Py<PyAny>) -> PyResult<()> {
650 if let Some(py_self) = self.python_instance()? {
651 Python::attach(|py| py_self.call_method1(py, "on_data", (data,)))?;
652 }
653 Ok(())
654 }
655
656 fn dispatch_on_signal(&mut self, signal: &Signal) -> PyResult<()> {
657 if let Some(py_self) = self.python_instance()? {
658 Python::attach(|py| {
659 py_self.call_method1(py, "on_signal", (signal.clone().into_py_any(py)?,))
660 })?;
661 }
662 Ok(())
663 }
664
665 fn dispatch_on_queue_state(&mut self, event: &QueueStateChanged) -> PyResult<()> {
666 if let Some(py_self) = self.python_instance()? {
667 Python::attach(|py| {
668 py_self.call_method1(py, "on_queue_state", (event.clone().into_py_any(py)?,))
669 })?;
670 }
671 Ok(())
672 }
673
674 fn dispatch_on_socket_state(&mut self, event: &SocketStateChanged) -> PyResult<()> {
675 if let Some(py_self) = self.python_instance()? {
676 Python::attach(|py| {
677 py_self.call_method1(py, "on_socket_state", (event.clone().into_py_any(py)?,))
678 })?;
679 }
680 Ok(())
681 }
682
683 fn dispatch_on_instrument(&mut self, instrument: Py<PyAny>) -> PyResult<()> {
684 if let Some(py_self) = self.python_instance()? {
685 Python::attach(|py| py_self.call_method1(py, "on_instrument", (instrument,)))?;
686 }
687 Ok(())
688 }
689
690 fn dispatch_on_quote(&mut self, quote: QuoteTick) -> PyResult<()> {
691 if let Some(py_self) = self.python_instance()? {
692 Python::attach(|py| py_self.call_method1(py, "on_quote", (quote.into_py_any(py)?,)))?;
693 }
694 Ok(())
695 }
696
697 fn dispatch_on_trade(&mut self, trade: TradeTick) -> PyResult<()> {
698 if let Some(py_self) = self.python_instance()? {
699 Python::attach(|py| py_self.call_method1(py, "on_trade", (trade.into_py_any(py)?,)))?;
700 }
701 Ok(())
702 }
703
704 fn dispatch_on_bar(&mut self, bar: Bar) -> PyResult<()> {
705 if let Some(py_self) = self.python_instance()? {
706 Python::attach(|py| py_self.call_method1(py, "on_bar", (bar.into_py_any(py)?,)))?;
707 }
708 Ok(())
709 }
710
711 fn dispatch_on_book_deltas(&mut self, deltas: &OrderBookDeltas) -> PyResult<()> {
712 if let Some(py_self) = self.python_instance()? {
713 Python::attach(|py| {
714 py_self.call_method1(py, "on_book_deltas", (deltas.clone().into_py_any(py)?,))
715 })?;
716 }
717 Ok(())
718 }
719
720 fn dispatch_on_book_depth(&mut self, depth: &OrderBookDepth) -> PyResult<()> {
721 if let Some(py_self) = self.python_instance()? {
722 Python::attach(|py| {
723 py_self.call_method1(py, "on_book_depth", (depth.clone().into_py_any(py)?,))
724 })?;
725 }
726 Ok(())
727 }
728
729 fn dispatch_on_book(&mut self, book: &OrderBook) -> PyResult<()> {
730 if let Some(py_self) = self.python_instance()? {
731 Python::attach(|py| {
732 py_self.call_method1(py, "on_book", (book.clone().into_py_any(py)?,))
733 })?;
734 }
735 Ok(())
736 }
737
738 fn dispatch_on_mark_price(&mut self, mark_price: MarkPriceUpdate) -> PyResult<()> {
739 if let Some(py_self) = self.python_instance()? {
740 Python::attach(|py| {
741 py_self.call_method1(py, "on_mark_price", (mark_price.into_py_any(py)?,))
742 })?;
743 }
744 Ok(())
745 }
746
747 fn dispatch_on_index_price(&mut self, index_price: IndexPriceUpdate) -> PyResult<()> {
748 if let Some(py_self) = self.python_instance()? {
749 Python::attach(|py| {
750 py_self.call_method1(py, "on_index_price", (index_price.into_py_any(py)?,))
751 })?;
752 }
753 Ok(())
754 }
755
756 fn dispatch_on_funding_rate(&mut self, funding_rate: FundingRateUpdate) -> PyResult<()> {
757 if let Some(py_self) = self.python_instance()? {
758 Python::attach(|py| {
759 py_self.call_method1(py, "on_funding_rate", (funding_rate.into_py_any(py)?,))
760 })?;
761 }
762 Ok(())
763 }
764
765 fn dispatch_on_instrument_status(&mut self, data: InstrumentStatus) -> PyResult<()> {
766 if let Some(py_self) = self.python_instance()? {
767 Python::attach(|py| {
768 py_self.call_method1(py, "on_instrument_status", (data.into_py_any(py)?,))
769 })?;
770 }
771 Ok(())
772 }
773
774 fn dispatch_on_instrument_close(&mut self, update: InstrumentClose) -> PyResult<()> {
775 if let Some(py_self) = self.python_instance()? {
776 Python::attach(|py| {
777 py_self.call_method1(py, "on_instrument_close", (update.into_py_any(py)?,))
778 })?;
779 }
780 Ok(())
781 }
782
783 fn dispatch_on_option_greeks(&mut self, greeks: OptionGreeks) -> PyResult<()> {
784 if let Some(py_self) = self.python_instance()? {
785 Python::attach(|py| {
786 py_self.call_method1(py, "on_option_greeks", (greeks.into_py_any(py)?,))
787 })?;
788 }
789 Ok(())
790 }
791
792 fn dispatch_on_option_chain(&mut self, slice: &OptionChainSlice) -> PyResult<()> {
793 if let Some(py_self) = self.python_instance()? {
794 Python::attach(|py| {
795 py_self.call_method1(py, "on_option_chain", (slice.clone().into_py_any(py)?,))
796 })?;
797 }
798 Ok(())
799 }
800
801 fn dispatch_on_historical_data(&mut self, data: Py<PyAny>) -> PyResult<()> {
802 if let Some(py_self) = self.python_instance()? {
803 Python::attach(|py| py_self.call_method1(py, "on_historical_data", (data,)))?;
804 }
805 Ok(())
806 }
807
808 fn dispatch_on_historical_book_deltas(&mut self, deltas: Vec<OrderBookDelta>) -> PyResult<()> {
809 if let Some(py_self) = self.python_instance()? {
810 Python::attach(|py| {
811 let py_deltas = deltas
812 .into_iter()
813 .map(|delta| delta.into_py_any(py))
814 .collect::<PyResult<Vec<_>>>()?;
815 py_self.call_method1(py, "on_historical_book_deltas", (py_deltas,))
816 })?;
817 }
818 Ok(())
819 }
820
821 fn dispatch_on_historical_book_depth(&mut self, depths: Vec<OrderBookDepth>) -> PyResult<()> {
822 if let Some(py_self) = self.python_instance()? {
823 Python::attach(|py| {
824 let py_depths = depths
825 .into_iter()
826 .map(|depth| depth.into_py_any(py))
827 .collect::<PyResult<Vec<_>>>()?;
828 py_self.call_method1(py, "on_historical_book_depth", (py_depths,))
829 })?;
830 }
831 Ok(())
832 }
833
834 fn dispatch_on_historical_quotes(&mut self, quotes: Vec<QuoteTick>) -> PyResult<()> {
835 if let Some(py_self) = self.python_instance()? {
836 Python::attach(|py| {
837 let py_quotes = quotes
838 .into_iter()
839 .map(|quote| quote.into_py_any(py))
840 .collect::<PyResult<Vec<_>>>()?;
841 py_self.call_method1(py, "on_historical_quotes", (py_quotes,))
842 })?;
843 }
844 Ok(())
845 }
846
847 fn dispatch_on_historical_trades(&mut self, trades: Vec<TradeTick>) -> PyResult<()> {
848 if let Some(py_self) = self.python_instance()? {
849 Python::attach(|py| {
850 let py_trades = trades
851 .into_iter()
852 .map(|trade| trade.into_py_any(py))
853 .collect::<PyResult<Vec<_>>>()?;
854 py_self.call_method1(py, "on_historical_trades", (py_trades,))
855 })?;
856 }
857 Ok(())
858 }
859
860 fn dispatch_on_historical_funding_rates(
861 &mut self,
862 funding_rates: Vec<FundingRateUpdate>,
863 ) -> PyResult<()> {
864 if let Some(py_self) = self.python_instance()? {
865 Python::attach(|py| {
866 let py_funding_rates = funding_rates
867 .into_iter()
868 .map(|rate| rate.into_py_any(py))
869 .collect::<PyResult<Vec<_>>>()?;
870 py_self.call_method1(py, "on_historical_funding_rates", (py_funding_rates,))
871 })?;
872 }
873 Ok(())
874 }
875
876 fn dispatch_on_historical_bars(&mut self, bars: Vec<Bar>) -> PyResult<()> {
877 if let Some(py_self) = self.python_instance()? {
878 Python::attach(|py| {
879 let py_bars = bars
880 .into_iter()
881 .map(|bar| bar.into_py_any(py))
882 .collect::<PyResult<Vec<_>>>()?;
883 py_self.call_method1(py, "on_historical_bars", (py_bars,))
884 })?;
885 }
886 Ok(())
887 }
888
889 fn dispatch_on_historical_mark_prices(
890 &mut self,
891 mark_prices: Vec<MarkPriceUpdate>,
892 ) -> PyResult<()> {
893 if let Some(py_self) = self.python_instance()? {
894 Python::attach(|py| {
895 let py_mark_prices = mark_prices
896 .into_iter()
897 .map(|price| price.into_py_any(py))
898 .collect::<PyResult<Vec<_>>>()?;
899 py_self.call_method1(py, "on_historical_mark_prices", (py_mark_prices,))
900 })?;
901 }
902 Ok(())
903 }
904
905 fn dispatch_on_historical_index_prices(
906 &mut self,
907 index_prices: Vec<IndexPriceUpdate>,
908 ) -> PyResult<()> {
909 if let Some(py_self) = self.python_instance()? {
910 Python::attach(|py| {
911 let py_index_prices = index_prices
912 .into_iter()
913 .map(|price| price.into_py_any(py))
914 .collect::<PyResult<Vec<_>>>()?;
915 py_self.call_method1(py, "on_historical_index_prices", (py_index_prices,))
916 })?;
917 }
918 Ok(())
919 }
920
921 fn python_instance(&self) -> PyResult<Option<Py<PyAny>>> {
924 upgrade_py_weakref(
925 self.py_self.as_ref(),
926 &DataActorNative::core(&self.core).actor_id,
927 )
928 }
929}
930
931impl Deref for PyStrategyInner {
932 type Target = DataActorCore;
933
934 fn deref(&self) -> &Self::Target {
935 DataActorNative::core(&self.core)
936 }
937}
938
939impl DerefMut for PyStrategyInner {
940 fn deref_mut(&mut self) -> &mut Self::Target {
941 DataActorNative::core_mut(&mut self.core)
942 }
943}
944
945impl DataActorNative for PyStrategyInner {
946 fn core(&self) -> &DataActorCore {
947 DataActorNative::core(&self.core)
948 }
949
950 fn core_mut(&mut self) -> &mut DataActorCore {
951 DataActorNative::core_mut(&mut self.core)
952 }
953}
954
955impl StrategyNative for PyStrategyInner {
956 fn strategy_core(&self) -> &StrategyCore {
957 &self.core
958 }
959
960 fn strategy_core_mut(&mut self) -> &mut StrategyCore {
961 &mut self.core
962 }
963}
964
965impl Strategy for PyStrategyInner {
966 fn external_order_instrument_ids(&self) -> Option<Vec<InstrumentId>> {
967 self.core.config.external_order_instrument_ids.clone()
968 }
969
970 fn on_market_exit(&mut self) {
971 self.logger
972 .log_callback_error("on_market_exit", self.dispatch_on_market_exit());
973 }
974
975 fn post_market_exit(&mut self) {
976 self.logger
977 .log_callback_error("post_market_exit", self.dispatch_post_market_exit());
978 }
979
980 fn on_order_initialized(&mut self, event: OrderInitialized) {
981 self.logger.log_callback_error(
982 "on_order_initialized",
983 self.dispatch_on_order_initialized(event),
984 );
985 }
986
987 fn on_order_event(&mut self, event: OrderEventAny) {
988 self.logger
989 .log_callback_error("on_order_event", self.dispatch_on_order_event(event));
990 }
991
992 fn on_order_denied(&mut self, event: OrderDenied) {
993 self.logger
994 .log_callback_error("on_order_denied", self.dispatch_on_order_denied(event));
995 }
996
997 fn on_order_emulated(&mut self, event: OrderEmulated) {
998 self.logger
999 .log_callback_error("on_order_emulated", self.dispatch_on_order_emulated(event));
1000 }
1001
1002 fn on_order_released(&mut self, event: OrderReleased) {
1003 self.logger
1004 .log_callback_error("on_order_released", self.dispatch_on_order_released(event));
1005 }
1006
1007 fn on_order_submitted(&mut self, event: OrderSubmitted) {
1008 self.logger.log_callback_error(
1009 "on_order_submitted",
1010 self.dispatch_on_order_submitted(event),
1011 );
1012 }
1013
1014 fn on_order_rejected(&mut self, event: OrderRejected) {
1015 self.logger
1016 .log_callback_error("on_order_rejected", self.dispatch_on_order_rejected(event));
1017 }
1018
1019 fn on_order_accepted(&mut self, event: OrderAccepted) {
1020 self.logger
1021 .log_callback_error("on_order_accepted", self.dispatch_on_order_accepted(event));
1022 }
1023
1024 fn on_order_expired(&mut self, event: OrderExpired) {
1025 self.logger
1026 .log_callback_error("on_order_expired", self.dispatch_on_order_expired(event));
1027 }
1028
1029 fn on_order_triggered(&mut self, event: OrderTriggered) {
1030 self.logger.log_callback_error(
1031 "on_order_triggered",
1032 self.dispatch_on_order_triggered(event),
1033 );
1034 }
1035
1036 fn on_order_pending_update(&mut self, event: OrderPendingUpdate) {
1037 self.logger.log_callback_error(
1038 "on_order_pending_update",
1039 self.dispatch_on_order_pending_update(event),
1040 );
1041 }
1042
1043 fn on_order_pending_cancel(&mut self, event: OrderPendingCancel) {
1044 self.logger.log_callback_error(
1045 "on_order_pending_cancel",
1046 self.dispatch_on_order_pending_cancel(event),
1047 );
1048 }
1049
1050 fn on_order_modify_rejected(&mut self, event: OrderModifyRejected) {
1051 self.logger.log_callback_error(
1052 "on_order_modify_rejected",
1053 self.dispatch_on_order_modify_rejected(event),
1054 );
1055 }
1056
1057 fn on_order_cancel_rejected(&mut self, event: OrderCancelRejected) {
1058 self.logger.log_callback_error(
1059 "on_order_cancel_rejected",
1060 self.dispatch_on_order_cancel_rejected(event),
1061 );
1062 }
1063
1064 fn on_order_updated(&mut self, event: OrderUpdated) {
1065 self.logger
1066 .log_callback_error("on_order_updated", self.dispatch_on_order_updated(&event));
1067 }
1068
1069 fn on_order_canceled(&mut self, event: &OrderCanceled) {
1070 self.logger
1071 .log_callback_error("on_order_canceled", self.dispatch_on_order_canceled(*event));
1072 }
1073
1074 fn on_order_filled(&mut self, event: &OrderFilled) {
1075 self.logger
1076 .log_callback_error("on_order_filled", self.dispatch_on_order_filled(event));
1077 }
1078
1079 fn on_order_fill_voided(&mut self, event: &OrderFillVoided) {
1080 self.logger.log_callback_error(
1081 "on_order_fill_voided",
1082 self.dispatch_on_order_fill_voided(event),
1083 );
1084 }
1085
1086 fn on_position_opened(&mut self, event: PositionOpened) {
1087 self.logger.log_callback_error(
1088 "on_position_opened",
1089 self.dispatch_on_position_opened(event),
1090 );
1091 }
1092
1093 fn on_position_event(&mut self, event: PositionEvent) {
1094 self.logger
1095 .log_callback_error("on_position_event", self.dispatch_on_position_event(event));
1096 }
1097
1098 fn on_position_changed(&mut self, event: PositionChanged) {
1099 self.logger.log_callback_error(
1100 "on_position_changed",
1101 self.dispatch_on_position_changed(event),
1102 );
1103 }
1104
1105 fn on_position_closed(&mut self, event: PositionClosed) {
1106 self.logger.log_callback_error(
1107 "on_position_closed",
1108 self.dispatch_on_position_closed(event),
1109 );
1110 }
1111}
1112
1113impl DataActor for PyStrategyInner {
1114 fn response_handler(&self) -> TypedHandler<DataResponseEnvelope> {
1115 nautilus_common::python::response::response_handler(
1116 self.core().actor_id().inner(),
1117 Self::python_instance,
1118 None,
1119 )
1120 }
1121
1122 fn on_start(&mut self) -> anyhow::Result<()> {
1123 Strategy::on_start(self)?;
1124 self.dispatch_on_start()
1125 .map_err(|e| anyhow::anyhow!("Python on_start failed:\n{}", format_exception(&e)))
1126 }
1127
1128 fn on_stop(&mut self) -> anyhow::Result<()> {
1129 self.dispatch_on_stop()
1130 .map_err(|e| anyhow::anyhow!("Python on_stop failed:\n{}", format_exception(&e)))
1131 }
1132
1133 fn on_resume(&mut self) -> anyhow::Result<()> {
1134 self.dispatch_on_resume()
1135 .map_err(|e| anyhow::anyhow!("Python on_resume failed:\n{}", format_exception(&e)))
1136 }
1137
1138 fn on_reset(&mut self) -> anyhow::Result<()> {
1139 self.dispatch_on_reset()
1140 .map_err(|e| anyhow::anyhow!("Python on_reset failed:\n{}", format_exception(&e)))
1141 }
1142
1143 fn on_dispose(&mut self) -> anyhow::Result<()> {
1144 self.dispatch_on_dispose()
1145 .map_err(|e| anyhow::anyhow!("Python on_dispose failed:\n{}", format_exception(&e)))
1146 }
1147
1148 fn on_degrade(&mut self) -> anyhow::Result<()> {
1149 self.dispatch_on_degrade()
1150 .map_err(|e| anyhow::anyhow!("Python on_degrade failed:\n{}", format_exception(&e)))
1151 }
1152
1153 fn on_fault(&mut self) -> anyhow::Result<()> {
1154 self.dispatch_on_fault()
1155 .map_err(|e| anyhow::anyhow!("Python on_fault failed:\n{}", format_exception(&e)))
1156 }
1157
1158 fn on_save(&self) -> anyhow::Result<IndexMap<String, Vec<u8>>> {
1159 self.dispatch_on_save()
1160 .map_err(|e| anyhow::anyhow!("Python on_save failed:\n{}", format_exception(&e)))
1161 }
1162
1163 fn on_load(&mut self, state: IndexMap<String, Vec<u8>>) -> anyhow::Result<()> {
1164 self.dispatch_on_load(&state)
1165 .map_err(|e| anyhow::anyhow!("Python on_load failed:\n{}", format_exception(&e)))
1166 }
1167
1168 fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()> {
1169 route_time_event(self, event);
1170 self.dispatch_on_time_event(event)
1171 .map_err(|e| anyhow::anyhow!("Python on_time_event failed:\n{}", format_exception(&e)))
1172 }
1173
1174 #[allow(unused_variables)]
1175 fn on_data(&mut self, data: &CustomData) -> anyhow::Result<()> {
1176 Python::attach(|py| {
1177 let py_data: Py<PyAny> = Py::new(py, data.clone())?.into_any();
1178 self.dispatch_on_data(py_data)
1179 .map_err(|e| anyhow::anyhow!("Python on_data failed:\n{}", format_exception(&e)))
1180 })
1181 }
1182
1183 fn on_signal(&mut self, signal: &Signal) -> anyhow::Result<()> {
1184 self.dispatch_on_signal(signal)
1185 .map_err(|e| anyhow::anyhow!("Python on_signal failed:\n{}", format_exception(&e)))
1186 }
1187
1188 fn on_queue_state(&mut self, event: &QueueStateChanged) -> anyhow::Result<()> {
1189 self.dispatch_on_queue_state(event)
1190 .map_err(|e| anyhow::anyhow!("Python on_queue_state failed:\n{}", format_exception(&e)))
1191 }
1192
1193 fn on_socket_state(&mut self, event: &SocketStateChanged) -> anyhow::Result<()> {
1194 self.dispatch_on_socket_state(event).map_err(|e| {
1195 anyhow::anyhow!("Python on_socket_state failed:\n{}", format_exception(&e))
1196 })
1197 }
1198
1199 fn on_instrument(&mut self, instrument: &InstrumentAny) -> anyhow::Result<()> {
1200 Python::attach(|py| {
1201 let py_instrument = instrument_any_to_pyobject(py, instrument.clone())
1202 .map_err(|e| anyhow::anyhow!("Failed to convert InstrumentAny to Python: {e}"))?;
1203 self.dispatch_on_instrument(py_instrument).map_err(|e| {
1204 anyhow::anyhow!("Python on_instrument failed:\n{}", format_exception(&e))
1205 })
1206 })
1207 }
1208
1209 fn on_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
1210 self.dispatch_on_quote(*quote)
1211 .map_err(|e| anyhow::anyhow!("Python on_quote failed:\n{}", format_exception(&e)))
1212 }
1213
1214 fn on_trade(&mut self, tick: &TradeTick) -> anyhow::Result<()> {
1215 self.dispatch_on_trade(*tick)
1216 .map_err(|e| anyhow::anyhow!("Python on_trade failed:\n{}", format_exception(&e)))
1217 }
1218
1219 fn on_bar(&mut self, bar: &Bar) -> anyhow::Result<()> {
1220 self.dispatch_on_bar(*bar)
1221 .map_err(|e| anyhow::anyhow!("Python on_bar failed:\n{}", format_exception(&e)))
1222 }
1223
1224 fn on_book_deltas(&mut self, deltas: &OrderBookDeltas) -> anyhow::Result<()> {
1225 self.dispatch_on_book_deltas(deltas)
1226 .map_err(|e| anyhow::anyhow!("Python on_book_deltas failed:\n{}", format_exception(&e)))
1227 }
1228
1229 fn on_book_depth(&mut self, depth: &OrderBookDepth) -> anyhow::Result<()> {
1230 self.dispatch_on_book_depth(depth)
1231 .map_err(|e| anyhow::anyhow!("Python on_book_depth failed:\n{}", format_exception(&e)))
1232 }
1233
1234 fn on_book(&mut self, order_book: &OrderBook) -> anyhow::Result<()> {
1235 self.dispatch_on_book(order_book)
1236 .map_err(|e| anyhow::anyhow!("Python on_book failed:\n{}", format_exception(&e)))
1237 }
1238
1239 fn on_mark_price(&mut self, mark_price: &MarkPriceUpdate) -> anyhow::Result<()> {
1240 self.dispatch_on_mark_price(*mark_price)
1241 .map_err(|e| anyhow::anyhow!("Python on_mark_price failed:\n{}", format_exception(&e)))
1242 }
1243
1244 fn on_index_price(&mut self, index_price: &IndexPriceUpdate) -> anyhow::Result<()> {
1245 self.dispatch_on_index_price(*index_price)
1246 .map_err(|e| anyhow::anyhow!("Python on_index_price failed:\n{}", format_exception(&e)))
1247 }
1248
1249 fn on_funding_rate(&mut self, funding_rate: &FundingRateUpdate) -> anyhow::Result<()> {
1250 self.dispatch_on_funding_rate(*funding_rate).map_err(|e| {
1251 anyhow::anyhow!("Python on_funding_rate failed:\n{}", format_exception(&e))
1252 })
1253 }
1254
1255 fn on_instrument_status(&mut self, data: &InstrumentStatus) -> anyhow::Result<()> {
1256 self.dispatch_on_instrument_status(*data).map_err(|e| {
1257 anyhow::anyhow!(
1258 "Python on_instrument_status failed:\n{}",
1259 format_exception(&e)
1260 )
1261 })
1262 }
1263
1264 fn on_instrument_close(&mut self, update: &InstrumentClose) -> anyhow::Result<()> {
1265 self.dispatch_on_instrument_close(*update).map_err(|e| {
1266 anyhow::anyhow!(
1267 "Python on_instrument_close failed:\n{}",
1268 format_exception(&e)
1269 )
1270 })
1271 }
1272
1273 fn on_option_greeks(&mut self, greeks: &OptionGreeks) -> anyhow::Result<()> {
1274 self.dispatch_on_option_greeks(*greeks).map_err(|e| {
1275 anyhow::anyhow!("Python on_option_greeks failed:\n{}", format_exception(&e))
1276 })
1277 }
1278
1279 fn on_option_chain(&mut self, slice: &OptionChainSlice) -> anyhow::Result<()> {
1280 self.dispatch_on_option_chain(slice).map_err(|e| {
1281 anyhow::anyhow!("Python on_option_chain failed:\n{}", format_exception(&e))
1282 })
1283 }
1284
1285 fn on_historical_data(&mut self, data: &dyn Any) -> anyhow::Result<()> {
1286 Python::attach(|py| {
1287 let py_data: Py<PyAny> = if let Some(custom_data) = data.downcast_ref::<CustomData>() {
1288 Py::new(py, custom_data.clone())?.into_any()
1289 } else if let Some(custom_data) = data.downcast_ref::<Vec<CustomData>>() {
1290 custom_data.clone().into_py_any(py)?
1291 } else {
1292 anyhow::bail!("Failed to convert historical data to Python: unsupported type");
1293 };
1294
1295 self.dispatch_on_historical_data(py_data).map_err(|e| {
1296 anyhow::anyhow!(
1297 "Python on_historical_data failed:\n{}",
1298 format_exception(&e)
1299 )
1300 })
1301 })
1302 }
1303
1304 fn on_historical_book_deltas(&mut self, deltas: &[OrderBookDelta]) -> anyhow::Result<()> {
1305 self.dispatch_on_historical_book_deltas(deltas.to_vec())
1306 .map_err(|e| {
1307 anyhow::anyhow!(
1308 "Python on_historical_book_deltas failed:\n{}",
1309 format_exception(&e)
1310 )
1311 })
1312 }
1313
1314 fn on_historical_book_depth(&mut self, depths: &[OrderBookDepth]) -> anyhow::Result<()> {
1315 self.dispatch_on_historical_book_depth(depths.to_vec())
1316 .map_err(|e| {
1317 anyhow::anyhow!(
1318 "Python on_historical_book_depth failed:\n{}",
1319 format_exception(&e)
1320 )
1321 })
1322 }
1323
1324 fn on_historical_quotes(&mut self, quotes: &[QuoteTick]) -> anyhow::Result<()> {
1325 self.dispatch_on_historical_quotes(quotes.to_vec())
1326 .map_err(|e| {
1327 anyhow::anyhow!(
1328 "Python on_historical_quotes failed:\n{}",
1329 format_exception(&e)
1330 )
1331 })
1332 }
1333
1334 fn on_historical_trades(&mut self, trades: &[TradeTick]) -> anyhow::Result<()> {
1335 self.dispatch_on_historical_trades(trades.to_vec())
1336 .map_err(|e| {
1337 anyhow::anyhow!(
1338 "Python on_historical_trades failed:\n{}",
1339 format_exception(&e)
1340 )
1341 })
1342 }
1343
1344 fn on_historical_funding_rates(
1345 &mut self,
1346 funding_rates: &[FundingRateUpdate],
1347 ) -> anyhow::Result<()> {
1348 self.dispatch_on_historical_funding_rates(funding_rates.to_vec())
1349 .map_err(|e| {
1350 anyhow::anyhow!(
1351 "Python on_historical_funding_rates failed:\n{}",
1352 format_exception(&e)
1353 )
1354 })
1355 }
1356
1357 fn on_historical_bars(&mut self, bars: &[Bar]) -> anyhow::Result<()> {
1358 self.dispatch_on_historical_bars(bars.to_vec())
1359 .map_err(|e| {
1360 anyhow::anyhow!(
1361 "Python on_historical_bars failed:\n{}",
1362 format_exception(&e)
1363 )
1364 })
1365 }
1366
1367 fn on_historical_mark_prices(&mut self, mark_prices: &[MarkPriceUpdate]) -> anyhow::Result<()> {
1368 self.dispatch_on_historical_mark_prices(mark_prices.to_vec())
1369 .map_err(|e| {
1370 anyhow::anyhow!(
1371 "Python on_historical_mark_prices failed:\n{}",
1372 format_exception(&e)
1373 )
1374 })
1375 }
1376
1377 fn on_historical_index_prices(
1378 &mut self,
1379 index_prices: &[IndexPriceUpdate],
1380 ) -> anyhow::Result<()> {
1381 self.dispatch_on_historical_index_prices(index_prices.to_vec())
1382 .map_err(|e| {
1383 anyhow::anyhow!(
1384 "Python on_historical_index_prices failed:\n{}",
1385 format_exception(&e)
1386 )
1387 })
1388 }
1389}
1390
1391fn state_to_pydict(py: Python<'_>, state: &IndexMap<String, Vec<u8>>) -> PyResult<Py<PyDict>> {
1392 let py_state = PyDict::new(py);
1393 for (key, value) in state {
1394 py_state.set_item(key, PyBytes::new(py, value))?;
1395 }
1396 Ok(py_state.unbind())
1397}
1398
1399fn pydict_to_state(state: &Bound<'_, PyDict>) -> PyResult<IndexMap<String, Vec<u8>>> {
1400 let mut rust_state = IndexMap::with_capacity(state.len());
1401 for (key, value) in state.iter() {
1402 rust_state.insert(key.extract()?, value.extract()?);
1403 }
1404 Ok(rust_state)
1405}
1406
1407#[allow(non_camel_case_types)]
1409#[pyo3::pyclass(
1410 module = "nautilus_trader.trading",
1411 name = "Strategy",
1412 unsendable,
1413 subclass,
1414 weakref
1415)]
1416#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.trading")]
1417pub struct PyStrategy {
1418 inner: Rc<UnsafeCell<PyStrategyInner>>,
1419}
1420
1421impl Debug for PyStrategy {
1422 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1423 f.debug_struct(stringify!(PyStrategy))
1424 .field("inner", &self.inner())
1425 .finish()
1426 }
1427}
1428
1429impl PyStrategy {
1430 #[inline]
1431 #[allow(unsafe_code)]
1432 pub(crate) fn inner(&self) -> &PyStrategyInner {
1433 unsafe { &*self.inner.get() }
1436 }
1437
1438 #[inline]
1439 #[allow(unsafe_code, clippy::mut_from_ref)]
1440 pub(crate) fn inner_mut(&self) -> &mut PyStrategyInner {
1441 unsafe { &mut *self.inner.get() }
1444 }
1445}
1446
1447impl PyStrategy {
1448 pub fn new_checked(config: Option<StrategyConfig>) -> CorrectnessResult<Self> {
1455 let config = config.unwrap_or_default();
1456 let core = StrategyCore::new_checked(config)?;
1457 let clock = PyClock::new_test();
1458 let logger = PyLogger::new(core.actor.actor_id.as_str());
1459
1460 let inner = PyStrategyInner {
1461 core,
1462 py_self: None,
1463 config: None,
1464 clock,
1465 logger,
1466 };
1467
1468 Ok(Self {
1469 inner: Rc::new(UnsafeCell::new(inner)),
1470 })
1471 }
1472
1473 #[must_use]
1480 pub fn new(config: Option<StrategyConfig>) -> Self {
1481 Self::new_checked(config).expect_display(FAILED)
1482 }
1483
1484 pub fn set_python_instance(&mut self, py_obj: &Bound<'_, PyAny>) -> PyResult<()> {
1493 self.inner_mut().py_self = Some(PyWeakrefReference::new(py_obj)?.unbind());
1494 Ok(())
1495 }
1496
1497 pub fn set_config(&mut self, config: Option<Py<PyAny>>) {
1502 self.inner_mut().config = config;
1503 }
1504
1505 pub fn set_external_order_instrument_ids(
1507 &mut self,
1508 external_order_instrument_ids: Option<Vec<InstrumentId>>,
1509 ) {
1510 self.inner_mut().core.config.external_order_instrument_ids = external_order_instrument_ids;
1511 }
1512
1513 #[must_use]
1515 pub fn external_order_instrument_ids(&self) -> Option<Vec<InstrumentId>> {
1516 self.inner().external_order_instrument_ids()
1517 }
1518
1519 pub fn set_actor_id(&mut self, actor_id: ActorId) {
1523 let inner = self.inner_mut();
1524 inner.core.actor.config.actor_id = Some(actor_id);
1525 inner.core.actor.actor_id = actor_id;
1526 inner.logger = PyLogger::new(actor_id.as_str());
1527 }
1528
1529 pub fn set_strategy_id(&mut self, strategy_id: StrategyId) -> anyhow::Result<()> {
1538 let inner = self.inner_mut();
1539 inner.core.change_id(strategy_id)?;
1540 inner.logger = PyLogger::new(inner.core.actor.actor_id.as_str());
1541 Ok(())
1542 }
1543
1544 pub fn set_order_id_tag(&mut self, order_id_tag: &str) -> anyhow::Result<()> {
1551 let inner = self.inner_mut();
1552 inner.core.change_order_id_tag(order_id_tag)?;
1553 inner.logger = PyLogger::new(inner.core.actor.actor_id.as_str());
1554 Ok(())
1555 }
1556
1557 pub fn set_log_events(&mut self, log_events: bool) {
1559 let inner = self.inner_mut();
1560 inner.core.actor.config.log_events = log_events;
1561 }
1562
1563 pub fn set_log_commands(&mut self, log_commands: bool) {
1565 let inner = self.inner_mut();
1566 inner.core.actor.config.log_commands = log_commands;
1567 }
1568
1569 #[must_use]
1574 pub fn strategy_id(&self) -> StrategyId {
1575 StrategyId::new(self.inner().core.actor.actor_id.inner())
1576 }
1577
1578 #[must_use]
1580 pub fn configured_strategy_id(&self) -> Option<StrategyId> {
1581 self.inner().core.strategy_id()
1582 }
1583
1584 #[must_use]
1586 pub fn order_id_tag(&self) -> Option<String> {
1587 self.inner().core.order_id_tag().map(str::to_string)
1588 }
1589
1590 #[must_use]
1592 pub fn is_registered(&self) -> bool {
1593 self.inner().core.actor.is_registered()
1594 }
1595
1596 pub fn register(
1602 &mut self,
1603 trader_id: TraderId,
1604 clock: Rc<RefCell<dyn Clock>>,
1605 cache: Rc<RefCell<Cache>>,
1606 portfolio: Rc<RefCell<Portfolio>>,
1607 ) -> anyhow::Result<()> {
1608 let inner = self.inner_mut();
1609 inner.core.register(trader_id, clock, cache, portfolio)?;
1610
1611 inner.clock = PyClock::from_rc(inner.core.actor.clock_rc());
1612
1613 let actor_id = inner.core.actor.actor_id.inner();
1614 let callback = TimeEventCallback::from(move |event: TimeEvent| {
1615 if let Some(mut strategy) = try_get_actor_unchecked::<PyStrategyInner>(&actor_id) {
1616 strategy.handle_time_event(&event);
1617 } else {
1618 log::error!("Strategy {actor_id} not found for time event handling");
1619 }
1620 });
1621
1622 inner.clock.inner_mut().register_default_handler(callback);
1623
1624 Component::initialize(inner)
1625 }
1626
1627 pub fn register_in_global_registries(&self) -> PyResult<()> {
1637 let inner = self.inner();
1638 let component_id = Component::component_id(inner);
1639 let actor_id = Actor::id(inner);
1640
1641 let Some(wrapper) = inner.python_instance()? else {
1642 return Err(to_pyruntime_err(format!(
1643 "Cannot register strategy {actor_id} without a Python wrapper, call `set_python_instance` first"
1644 )));
1645 };
1646
1647 let inner_ref: Rc<UnsafeCell<PyStrategyInner>> = self.inner.clone();
1648
1649 let component_trait_ref: Rc<UnsafeCell<dyn Component>> = inner_ref.clone();
1650 with_component_registry(|registry| {
1651 registry.insert(component_id.inner(), component_trait_ref);
1652 });
1653
1654 let actor_trait_ref: Rc<UnsafeCell<dyn Actor>> = inner_ref;
1655 with_actor_registry(|registry| registry.insert(actor_id, actor_trait_ref));
1656
1657 retain_python_wrapper(component_id, wrapper, inner.core.actor.message_bus());
1658
1659 Ok(())
1660 }
1661}
1662
1663#[pyo3::pymethods]
1664#[pyo3_stub_gen::derive::gen_stub_pymethods]
1665#[allow(
1666 clippy::large_types_passed_by_value,
1667 reason = "PyO3 callbacks accept Python-owned event values"
1668)]
1669impl PyStrategy {
1670 #[new]
1682 #[pyo3(signature = (config=None))]
1683 fn py_new(config: Option<Py<PyAny>>) -> PyResult<Self> {
1684 let strategy_config = config
1685 .as_ref()
1686 .and_then(|obj| Python::attach(|py| obj.extract::<StrategyConfig>(py).ok()));
1687 let mut strategy = Self::new_checked(strategy_config).map_err(to_pyvalue_err)?;
1688 strategy.set_config(config);
1689 Ok(strategy)
1690 }
1691
1692 #[pyo3(signature = (config=None))]
1694 fn __init__(slf: &Bound<'_, Self>, config: Option<Py<PyAny>>) -> PyResult<()> {
1695 {
1696 let mut borrowed = slf.borrow_mut();
1697 borrowed.set_python_instance(slf.as_any())?;
1698 if config.is_some() {
1700 borrowed.set_config(config);
1701 }
1702 }
1703
1704 if !has_configured_strategy_id(slf) {
1705 let py_type = slf.get_type();
1706 let type_name = py_type.name()?;
1707 let actor_id = class_derived_actor_id(slf, type_name.to_str()?)?;
1708 slf.borrow_mut().set_actor_id(actor_id);
1709 }
1710
1711 Ok(())
1712 }
1713
1714 #[getter]
1715 #[pyo3(name = "trader_id")]
1716 fn py_trader_id(&self) -> Option<TraderId> {
1717 self.inner().core.trader_id()
1718 }
1719
1720 #[getter]
1721 #[pyo3(name = "strategy_id")]
1722 fn py_strategy_id(&self) -> StrategyId {
1723 self.strategy_id()
1724 }
1725
1726 #[getter]
1727 #[pyo3(name = "config")]
1728 fn py_config(&self, py: Python<'_>) -> Option<Py<PyAny>> {
1729 self.inner()
1730 .config
1731 .as_ref()
1732 .map(|config| config.clone_ref(py))
1733 }
1734
1735 #[getter]
1736 #[pyo3(name = "clock")]
1737 fn py_clock(&self) -> PyResult<PyClock> {
1738 let inner = self.inner();
1739 if inner.core.actor.is_registered() {
1740 Ok(inner.clock.clone())
1741 } else {
1742 Err(to_pyruntime_err(
1743 "Strategy must be registered with a trader before accessing clock",
1744 ))
1745 }
1746 }
1747
1748 #[getter]
1749 #[pyo3(name = "cache")]
1750 fn py_cache(&self) -> PyResult<PyCache> {
1751 let inner = self.inner();
1752 if inner.core.actor.is_registered() {
1753 Ok(PyCache::from_rc(inner.core.actor.cache_rc()))
1754 } else {
1755 Err(to_pyruntime_err(
1756 "Strategy must be registered with a trader before accessing cache",
1757 ))
1758 }
1759 }
1760
1761 #[pyo3(name = "set_external_order_instrument_ids")]
1771 fn py_set_external_order_instrument_ids(
1772 &mut self,
1773 instrument_ids: Vec<InstrumentId>,
1774 ) -> PyResult<()> {
1775 Strategy::set_external_order_instrument_ids(self.inner_mut(), instrument_ids)
1776 .map_err(to_pyruntime_err)
1777 }
1778
1779 #[getter]
1780 #[pyo3(name = "portfolio")]
1781 fn py_portfolio(&self) -> PyResult<PyPortfolio> {
1782 let inner = self.inner();
1783 if inner.core.actor.is_registered() {
1784 Ok(PyPortfolio::from_rc(inner.portfolio_rc()))
1785 } else {
1786 Err(to_pyruntime_err(
1787 "Strategy must be registered with a trader before accessing portfolio",
1788 ))
1789 }
1790 }
1791
1792 #[getter]
1793 #[pyo3(name = "order_factory")]
1794 fn py_order_factory(&self) -> PyResult<PyOrderFactory> {
1795 let inner = self.inner();
1796 if inner.core.actor.is_registered() {
1797 Ok(PyOrderFactory::from_rc(inner.order_factory_rc()))
1798 } else {
1799 Err(to_pyruntime_err(
1800 "Strategy must be registered with a trader before accessing order_factory",
1801 ))
1802 }
1803 }
1804
1805 #[getter]
1806 #[pyo3(name = "log")]
1807 fn py_log(&self) -> PyLogger {
1808 self.inner().logger.clone()
1809 }
1810
1811 #[pyo3(name = "state")]
1812 fn py_state(&self) -> ComponentState {
1813 self.inner().core.actor.state()
1814 }
1815
1816 #[pyo3(name = "is_ready")]
1817 fn py_is_ready(&self) -> bool {
1818 Component::is_ready(self.inner())
1819 }
1820
1821 #[pyo3(name = "is_running")]
1822 fn py_is_running(&self) -> bool {
1823 Component::is_running(self.inner())
1824 }
1825
1826 #[pyo3(name = "is_stopped")]
1827 fn py_is_stopped(&self) -> bool {
1828 Component::is_stopped(self.inner())
1829 }
1830
1831 #[pyo3(name = "is_disposed")]
1832 fn py_is_disposed(&self) -> bool {
1833 Component::is_disposed(self.inner())
1834 }
1835
1836 #[pyo3(name = "is_degraded")]
1837 fn py_is_degraded(&self) -> bool {
1838 Component::is_degraded(self.inner())
1839 }
1840
1841 #[pyo3(name = "is_faulted")]
1842 fn py_is_faulted(&self) -> bool {
1843 Component::is_faulted(self.inner())
1844 }
1845
1846 #[pyo3(name = "start")]
1847 fn py_start(&mut self) -> PyResult<()> {
1848 Component::start(self.inner_mut()).map_err(to_pyruntime_err)
1849 }
1850
1851 #[pyo3(name = "stop")]
1852 fn py_stop(&mut self) -> PyResult<()> {
1853 let inner = self.inner_mut();
1854 if Strategy::stop(inner) {
1855 Component::stop(inner).map_err(to_pyruntime_err)
1856 } else {
1857 Ok(())
1858 }
1859 }
1860
1861 #[pyo3(name = "market_exit")]
1862 fn py_market_exit(&mut self) -> PyResult<()> {
1863 Strategy::market_exit(self.inner_mut()).map_err(to_pyruntime_err)
1864 }
1865
1866 #[pyo3(name = "is_exiting")]
1867 fn py_is_exiting(&self) -> bool {
1868 Strategy::is_exiting(self.inner())
1869 }
1870
1871 #[pyo3(name = "save")]
1872 fn py_save(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
1873 let state = DataActor::on_save(self.inner()).map_err(to_pyruntime_err)?;
1874 state_to_pydict(py, &state)
1875 }
1876
1877 #[pyo3(name = "load")]
1878 fn py_load(&mut self, state: &Bound<'_, PyDict>) -> PyResult<()> {
1879 let state = pydict_to_state(state)?;
1880 DataActor::on_load(self.inner_mut(), state).map_err(to_pyruntime_err)
1881 }
1882
1883 #[pyo3(name = "publish_data")]
1884 fn py_publish_data(&self, data_type: &DataType, data: &CustomData) -> PyResult<()> {
1885 self.ensure_registered_for_data()?;
1886 DataActor::publish_data(self.inner(), data_type, data);
1887 Ok(())
1888 }
1889
1890 #[pyo3(name = "publish_signal")]
1891 #[pyo3(signature = (name, value, ts_event=0))]
1892 #[expect(
1893 clippy::needless_pass_by_value,
1894 reason = "PyO3 accepts an owned PyAny handle for Python signal values"
1895 )]
1896 fn py_publish_signal(
1897 &self,
1898 py: Python<'_>,
1899 name: &str,
1900 value: Py<PyAny>,
1901 ts_event: u64,
1902 ) -> PyResult<()> {
1903 self.ensure_registered_for_data()?;
1904 let value_str: String = value.bind(py).str()?.extract()?;
1905 DataActor::publish_signal(self.inner(), name, value_str, UnixNanos::from(ts_event));
1906 Ok(())
1907 }
1908
1909 #[pyo3(name = "add_synthetic")]
1910 fn py_add_synthetic(&self, synthetic: SyntheticInstrument) -> PyResult<()> {
1911 self.ensure_registered_for_data()?;
1912 DataActor::add_synthetic(self.inner(), synthetic).map_err(to_pyvalue_err)
1913 }
1914
1915 #[pyo3(name = "update_synthetic")]
1916 fn py_update_synthetic(&self, synthetic: SyntheticInstrument) -> PyResult<()> {
1917 self.ensure_registered_for_data()?;
1918 DataActor::update_synthetic(self.inner(), synthetic).map_err(to_pyvalue_err)
1919 }
1920
1921 #[pyo3(name = "resume")]
1922 fn py_resume(&mut self) -> PyResult<()> {
1923 Component::resume(self.inner_mut()).map_err(to_pyruntime_err)
1924 }
1925
1926 #[pyo3(name = "reset")]
1927 fn py_reset(&mut self) -> PyResult<()> {
1928 Component::reset(self.inner_mut()).map_err(to_pyruntime_err)
1929 }
1930
1931 #[pyo3(name = "dispose")]
1932 fn py_dispose(&mut self) -> PyResult<()> {
1933 Component::dispose(self.inner_mut()).map_err(to_pyruntime_err)
1934 }
1935
1936 #[pyo3(name = "degrade")]
1937 fn py_degrade(&mut self) -> PyResult<()> {
1938 Component::degrade(self.inner_mut()).map_err(to_pyruntime_err)
1939 }
1940
1941 #[pyo3(name = "fault")]
1942 fn py_fault(&mut self) -> PyResult<()> {
1943 Component::fault(self.inner_mut()).map_err(to_pyruntime_err)
1944 }
1945
1946 #[pyo3(name = "shutdown_system")]
1947 #[pyo3(signature = (reason=None))]
1948 fn py_shutdown_system(&self, reason: Option<String>) -> PyResult<()> {
1949 let inner = self.inner();
1950 if !inner.core.actor.is_registered() {
1951 return Err(to_pyruntime_err(
1952 "Strategy must be registered with a trader before shutting down the system",
1953 ));
1954 }
1955
1956 DataActor::shutdown_system(inner, reason);
1957 Ok(())
1958 }
1959
1960 #[getter]
1961 #[pyo3(name = "registered_indicators")]
1962 fn py_registered_indicators(&self, py: Python<'_>) -> PyResult<Py<PyList>> {
1963 let inner = self.inner();
1964 registered_python_indicators(
1965 py,
1966 DataActorNative::core(&inner.core).registered_indicators(),
1967 )
1968 }
1969
1970 #[pyo3(name = "indicators_initialized")]
1971 fn py_indicators_initialized(&self, _py: Python<'_>) -> PyResult<bool> {
1972 let inner = self.inner();
1973 DataActorNative::core(&inner.core)
1974 .indicators_initialized()
1975 .map_err(to_pyruntime_err)
1976 }
1977
1978 #[pyo3(name = "register_indicator_for_quote_ticks")]
1979 fn py_register_indicator_for_quote_ticks(
1980 &mut self,
1981 py: Python<'_>,
1982 instrument_id: InstrumentId,
1983 indicator: Py<PyAny>,
1984 ) {
1985 let indicator = wrap_python_indicator(py, indicator);
1986 let inner = self.inner_mut();
1987 DataActorNative::core_mut(&mut inner.core)
1988 .register_indicator_for_quote_ticks(instrument_id, indicator);
1989 }
1990
1991 #[pyo3(name = "register_indicator_for_trade_ticks")]
1992 fn py_register_indicator_for_trade_ticks(
1993 &mut self,
1994 py: Python<'_>,
1995 instrument_id: InstrumentId,
1996 indicator: Py<PyAny>,
1997 ) {
1998 let indicator = wrap_python_indicator(py, indicator);
1999 let inner = self.inner_mut();
2000 DataActorNative::core_mut(&mut inner.core)
2001 .register_indicator_for_trade_ticks(instrument_id, indicator);
2002 }
2003
2004 #[pyo3(name = "register_indicator_for_bars")]
2005 fn py_register_indicator_for_bars(
2006 &mut self,
2007 py: Python<'_>,
2008 bar_type: BarType,
2009 indicator: Py<PyAny>,
2010 ) {
2011 let indicator = wrap_python_indicator(py, indicator);
2012 let inner = self.inner_mut();
2013 DataActorNative::core_mut(&mut inner.core).register_indicator_for_bars(bar_type, indicator);
2014 }
2015
2016 #[pyo3(name = "submit_order")]
2017 #[pyo3(signature = (order, position_id=None, client_id=None, params=None))]
2018 fn py_submit_order(
2019 &mut self,
2020 py: Python<'_>,
2021 order: Py<PyAny>,
2022 position_id: Option<PositionId>,
2023 client_id: Option<ClientId>,
2024 params: Option<Py<PyDict>>,
2025 ) -> PyResult<()> {
2026 let order = pyobject_to_order_any(py, order)?;
2027 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2028 match params {
2029 Some(dict) => from_pydict(py, &dict),
2030 None => Ok(None),
2031 }
2032 })?;
2033 let inner = self.inner_mut();
2034
2035 Strategy::submit_order(inner, order, position_id, client_id, params_map)
2036 .map_err(to_pyruntime_err)
2037 }
2038
2039 #[pyo3(name = "submit_order_list")]
2040 #[pyo3(signature = (order_list, position_id=None, client_id=None, params=None))]
2041 #[expect(
2042 clippy::needless_pass_by_value,
2043 reason = "PyO3 owns extracted method arguments before Rust conversion"
2044 )]
2045 fn py_submit_order_list(
2046 &mut self,
2047 py: Python<'_>,
2048 order_list: Py<PyAny>,
2049 position_id: Option<PositionId>,
2050 client_id: Option<ClientId>,
2051 params: Option<Py<PyDict>>,
2052 ) -> PyResult<()> {
2053 let orders = py_order_list_to_orders(py, &order_list)?;
2054 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2055 match params {
2056 Some(dict) => from_pydict(py, &dict),
2057 None => Ok(None),
2058 }
2059 })?;
2060 let inner = self.inner_mut();
2061
2062 Strategy::submit_order_list(inner, orders, position_id, client_id, params_map)
2063 .map_err(to_pyruntime_err)
2064 }
2065
2066 #[pyo3(name = "modify_order")]
2067 #[pyo3(signature = (client_order_id, quantity=None, price=None, trigger_price=None, client_id=None, params=None))]
2068 fn py_modify_order(
2069 &mut self,
2070 client_order_id: ClientOrderId,
2071 quantity: Option<Quantity>,
2072 price: Option<Price>,
2073 trigger_price: Option<Price>,
2074 client_id: Option<ClientId>,
2075 params: Option<Py<PyDict>>,
2076 ) -> PyResult<()> {
2077 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2078 match params {
2079 Some(dict) => from_pydict(py, &dict),
2080 None => Ok(None),
2081 }
2082 })?;
2083 let inner = self.inner_mut();
2084
2085 Strategy::modify_order(
2086 inner,
2087 client_order_id,
2088 quantity,
2089 price,
2090 trigger_price,
2091 client_id,
2092 params_map,
2093 )
2094 .map_err(to_pyruntime_err)
2095 }
2096
2097 #[pyo3(name = "modify_orders")]
2098 #[pyo3(signature = (updates, client_id=None, params=None))]
2099 fn py_modify_orders(
2100 &mut self,
2101 updates: Vec<BatchModifyOrder>,
2102 client_id: Option<ClientId>,
2103 params: Option<Py<PyDict>>,
2104 ) -> PyResult<()> {
2105 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2106 match params {
2107 Some(dict) => from_pydict(py, &dict),
2108 None => Ok(None),
2109 }
2110 })?;
2111
2112 Strategy::modify_orders(self.inner_mut(), updates, client_id, params_map)
2113 .map_err(to_pyruntime_err)
2114 }
2115
2116 #[pyo3(name = "cancel_order")]
2117 #[pyo3(signature = (client_order_id, client_id=None, params=None))]
2118 fn py_cancel_order(
2119 &mut self,
2120 client_order_id: ClientOrderId,
2121 client_id: Option<ClientId>,
2122 params: Option<Py<PyDict>>,
2123 ) -> PyResult<()> {
2124 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2125 match params {
2126 Some(dict) => from_pydict(py, &dict),
2127 None => Ok(None),
2128 }
2129 })?;
2130 let inner = self.inner_mut();
2131
2132 Strategy::cancel_order(inner, client_order_id, client_id, params_map)
2133 .map_err(to_pyruntime_err)
2134 }
2135
2136 #[pyo3(name = "cancel_gtd_expiry")]
2138 #[pyo3(signature = (order))]
2139 fn py_cancel_gtd_expiry(&mut self, py: Python<'_>, order: Py<PyAny>) -> PyResult<()> {
2140 let order = pyobject_to_order_any(py, order)?;
2141
2142 Strategy::cancel_gtd_expiry(self.inner_mut(), &order.client_order_id());
2143 Ok(())
2144 }
2145
2146 #[pyo3(name = "cancel_orders")]
2147 #[pyo3(signature = (client_order_ids, client_id=None, params=None))]
2148 fn py_cancel_orders(
2149 &mut self,
2150 client_order_ids: Vec<ClientOrderId>,
2151 client_id: Option<ClientId>,
2152 params: Option<Py<PyDict>>,
2153 ) -> PyResult<()> {
2154 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2155 match params {
2156 Some(dict) => from_pydict(py, &dict),
2157 None => Ok(None),
2158 }
2159 })?;
2160
2161 Strategy::cancel_orders(self.inner_mut(), client_order_ids, client_id, params_map)
2162 .map_err(to_pyruntime_err)
2163 }
2164
2165 #[pyo3(name = "cancel_all_orders")]
2166 #[pyo3(signature = (instrument_id, order_side=None, client_id=None, strategy_only=true, params=None))]
2167 fn py_cancel_all_orders(
2168 &mut self,
2169 instrument_id: InstrumentId,
2170 order_side: Option<OrderSide>,
2171 client_id: Option<ClientId>,
2172 strategy_only: bool,
2173 params: Option<Py<PyDict>>,
2174 ) -> PyResult<()> {
2175 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2176 match params {
2177 Some(dict) => from_pydict(py, &dict),
2178 None => Ok(None),
2179 }
2180 })?;
2181 Strategy::cancel_all_orders(
2182 self.inner_mut(),
2183 instrument_id,
2184 order_side,
2185 client_id,
2186 strategy_only,
2187 params_map,
2188 )
2189 .map_err(to_pyruntime_err)
2190 }
2191
2192 #[pyo3(name = "close_position")]
2193 #[pyo3(signature = (position, client_id=None, tags=None, time_in_force=None, reduce_only=None, quote_quantity=None, params=None))]
2194 #[expect(clippy::too_many_arguments)]
2195 fn py_close_position(
2196 &mut self,
2197 position: &Position,
2198 client_id: Option<ClientId>,
2199 tags: Option<Vec<String>>,
2200 time_in_force: Option<TimeInForce>,
2201 reduce_only: Option<bool>,
2202 quote_quantity: Option<bool>,
2203 params: Option<Py<PyDict>>,
2204 ) -> PyResult<()> {
2205 let tags = tags.map(|t| t.into_iter().map(|s| Ustr::from(&s)).collect());
2206 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2207 match params {
2208 Some(dict) => from_pydict(py, &dict),
2209 None => Ok(None),
2210 }
2211 })?;
2212 Strategy::close_position(
2213 self.inner_mut(),
2214 position,
2215 client_id,
2216 tags,
2217 time_in_force,
2218 reduce_only,
2219 quote_quantity,
2220 params_map,
2221 )
2222 .map_err(to_pyruntime_err)
2223 }
2224
2225 #[pyo3(name = "close_all_positions")]
2226 #[pyo3(signature = (instrument_id, position_side=None, client_id=None, tags=None, time_in_force=None, reduce_only=None, quote_quantity=None, params=None))]
2227 #[expect(clippy::too_many_arguments)]
2228 fn py_close_all_positions(
2229 &mut self,
2230 instrument_id: InstrumentId,
2231 position_side: Option<PositionSide>,
2232 client_id: Option<ClientId>,
2233 tags: Option<Vec<String>>,
2234 time_in_force: Option<TimeInForce>,
2235 reduce_only: Option<bool>,
2236 quote_quantity: Option<bool>,
2237 params: Option<Py<PyDict>>,
2238 ) -> PyResult<()> {
2239 let tags = tags.map(|t| t.into_iter().map(|s| Ustr::from(&s)).collect());
2240 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2241 match params {
2242 Some(dict) => from_pydict(py, &dict),
2243 None => Ok(None),
2244 }
2245 })?;
2246 Strategy::close_all_positions(
2247 self.inner_mut(),
2248 instrument_id,
2249 position_side,
2250 client_id,
2251 tags,
2252 time_in_force,
2253 reduce_only,
2254 quote_quantity,
2255 params_map,
2256 )
2257 .map_err(to_pyruntime_err)
2258 }
2259
2260 #[pyo3(name = "query_account")]
2261 #[pyo3(signature = (account_id, client_id=None, params=None))]
2262 fn py_query_account(
2263 &mut self,
2264 py: Python<'_>,
2265 account_id: AccountId,
2266 client_id: Option<ClientId>,
2267 params: Option<Py<PyDict>>,
2268 ) -> PyResult<()> {
2269 let params_map = match params {
2270 Some(dict) => from_pydict(py, &dict)?,
2271 None => None,
2272 };
2273 Strategy::query_account(self.inner_mut(), account_id, client_id, params_map)
2274 .map_err(to_pyruntime_err)
2275 }
2276
2277 #[pyo3(name = "query_order")]
2278 #[pyo3(signature = (order, client_id=None, params=None))]
2279 fn py_query_order(
2280 &mut self,
2281 py: Python<'_>,
2282 order: Py<PyAny>,
2283 client_id: Option<ClientId>,
2284 params: Option<Py<PyDict>>,
2285 ) -> PyResult<()> {
2286 let order = pyobject_to_order_any(py, order)?;
2287 let params_map = match params {
2288 Some(dict) => from_pydict(py, &dict)?,
2289 None => None,
2290 };
2291 Strategy::query_order(self.inner_mut(), &order, client_id, params_map)
2292 .map_err(to_pyruntime_err)
2293 }
2294
2295 #[pyo3(name = "on_start")]
2296 fn py_on_start(_slf: &Bound<'_, Self>) {}
2297
2298 #[pyo3(name = "on_stop")]
2299 fn py_on_stop(_slf: &Bound<'_, Self>) {}
2300
2301 #[pyo3(name = "on_resume")]
2302 fn py_on_resume(_slf: &Bound<'_, Self>) {}
2303
2304 #[pyo3(name = "on_reset")]
2305 fn py_on_reset(_slf: &Bound<'_, Self>) {}
2306
2307 #[pyo3(name = "on_dispose")]
2308 fn py_on_dispose(_slf: &Bound<'_, Self>) {}
2309
2310 #[pyo3(name = "on_degrade")]
2311 fn py_on_degrade(_slf: &Bound<'_, Self>) {}
2312
2313 #[pyo3(name = "on_fault")]
2314 fn py_on_fault(_slf: &Bound<'_, Self>) {}
2315
2316 #[pyo3(name = "on_save")]
2317 fn py_on_save(_slf: &Bound<'_, Self>, py: Python<'_>) -> Py<PyDict> {
2318 PyDict::new(py).unbind()
2319 }
2320
2321 #[allow(unused_variables)]
2322 #[pyo3(name = "on_load")]
2323 fn py_on_load(_slf: &Bound<'_, Self>, state: &Bound<'_, PyDict>) {}
2324
2325 #[allow(unused_variables, clippy::needless_pass_by_value)]
2326 #[pyo3(name = "on_time_event")]
2327 fn py_on_time_event(_slf: &Bound<'_, Self>, event: TimeEvent) {}
2328
2329 #[allow(unused_variables, clippy::needless_pass_by_value)]
2330 #[pyo3(name = "on_data")]
2331 fn py_on_data(_slf: &Bound<'_, Self>, data: Py<PyAny>) {}
2332
2333 #[allow(unused_variables)]
2334 #[pyo3(name = "on_signal")]
2335 fn py_on_signal(_slf: &Bound<'_, Self>, signal: &Signal) {}
2336
2337 #[allow(unused_variables, clippy::needless_pass_by_value)]
2338 #[pyo3(name = "on_queue_state")]
2339 fn py_on_queue_state(_slf: &Bound<'_, Self>, event: QueueStateChanged) {}
2340
2341 #[allow(unused_variables, clippy::needless_pass_by_value)]
2342 #[pyo3(name = "on_socket_state")]
2343 fn py_on_socket_state(_slf: &Bound<'_, Self>, event: SocketStateChanged) {}
2344
2345 #[allow(unused_variables, clippy::needless_pass_by_value)]
2346 #[pyo3(name = "on_instrument")]
2347 fn py_on_instrument(_slf: &Bound<'_, Self>, instrument: Py<PyAny>) {}
2348
2349 #[allow(unused_variables)]
2350 #[pyo3(name = "on_quote")]
2351 fn py_on_quote(_slf: &Bound<'_, Self>, quote: QuoteTick) {}
2352
2353 #[allow(unused_variables)]
2354 #[pyo3(name = "on_trade")]
2355 fn py_on_trade(_slf: &Bound<'_, Self>, trade: TradeTick) {}
2356
2357 #[allow(unused_variables)]
2358 #[pyo3(name = "on_bar")]
2359 fn py_on_bar(_slf: &Bound<'_, Self>, bar: Bar) {}
2360
2361 #[allow(unused_variables, clippy::needless_pass_by_value)]
2362 #[pyo3(name = "on_book_deltas")]
2363 fn py_on_book_deltas(_slf: &Bound<'_, Self>, deltas: OrderBookDeltas) {}
2364
2365 #[allow(unused_variables)]
2366 #[pyo3(name = "on_book_depth")]
2367 fn py_on_book_depth(_slf: &Bound<'_, Self>, depth: &OrderBookDepth) {}
2368
2369 #[allow(unused_variables)]
2370 #[pyo3(name = "on_book")]
2371 fn py_on_book(_slf: &Bound<'_, Self>, book: &OrderBook) {}
2372
2373 #[allow(unused_variables)]
2374 #[pyo3(name = "on_mark_price")]
2375 fn py_on_mark_price(_slf: &Bound<'_, Self>, mark_price: MarkPriceUpdate) {}
2376
2377 #[allow(unused_variables)]
2378 #[pyo3(name = "on_index_price")]
2379 fn py_on_index_price(_slf: &Bound<'_, Self>, index_price: IndexPriceUpdate) {}
2380
2381 #[allow(unused_variables)]
2382 #[pyo3(name = "on_funding_rate")]
2383 fn py_on_funding_rate(_slf: &Bound<'_, Self>, funding_rate: FundingRateUpdate) {}
2384
2385 #[allow(unused_variables)]
2386 #[pyo3(name = "on_instrument_status")]
2387 fn py_on_instrument_status(_slf: &Bound<'_, Self>, status: InstrumentStatus) {}
2388
2389 #[allow(unused_variables)]
2390 #[pyo3(name = "on_instrument_close")]
2391 fn py_on_instrument_close(_slf: &Bound<'_, Self>, close: InstrumentClose) {}
2392
2393 #[allow(unused_variables)]
2394 #[pyo3(name = "on_option_greeks")]
2395 fn py_on_option_greeks(_slf: &Bound<'_, Self>, greeks: OptionGreeks) {}
2396
2397 #[allow(unused_variables, clippy::needless_pass_by_value)]
2398 #[pyo3(name = "on_option_chain")]
2399 fn py_on_option_chain(_slf: &Bound<'_, Self>, slice: OptionChainSlice) {}
2400
2401 #[pyo3(name = "on_market_exit")]
2402 fn py_on_market_exit(_slf: &Bound<'_, Self>) {}
2403
2404 #[pyo3(name = "post_market_exit")]
2405 fn py_post_market_exit(_slf: &Bound<'_, Self>) {}
2406
2407 #[allow(unused_variables, clippy::needless_pass_by_value)]
2408 #[pyo3(name = "on_order_initialized")]
2409 fn py_on_order_initialized(_slf: &Bound<'_, Self>, event: OrderInitialized) {}
2410
2411 #[allow(unused_variables, clippy::needless_pass_by_value)]
2412 #[pyo3(name = "on_order_event")]
2413 fn py_on_order_event(_slf: &Bound<'_, Self>, event: Py<PyAny>) {}
2414
2415 #[allow(unused_variables)]
2416 #[pyo3(name = "on_order_denied")]
2417 fn py_on_order_denied(_slf: &Bound<'_, Self>, event: OrderDenied) {}
2418
2419 #[allow(unused_variables)]
2420 #[pyo3(name = "on_order_emulated")]
2421 fn py_on_order_emulated(_slf: &Bound<'_, Self>, event: OrderEmulated) {}
2422
2423 #[allow(unused_variables)]
2424 #[pyo3(name = "on_order_released")]
2425 fn py_on_order_released(_slf: &Bound<'_, Self>, event: OrderReleased) {}
2426
2427 #[allow(unused_variables)]
2428 #[pyo3(name = "on_order_submitted")]
2429 fn py_on_order_submitted(_slf: &Bound<'_, Self>, event: OrderSubmitted) {}
2430
2431 #[allow(unused_variables)]
2432 #[pyo3(name = "on_order_rejected")]
2433 fn py_on_order_rejected(_slf: &Bound<'_, Self>, event: OrderRejected) {}
2434
2435 #[allow(unused_variables)]
2436 #[pyo3(name = "on_order_accepted")]
2437 fn py_on_order_accepted(_slf: &Bound<'_, Self>, event: OrderAccepted) {}
2438
2439 #[allow(unused_variables)]
2440 #[pyo3(name = "on_order_expired")]
2441 fn py_on_order_expired(_slf: &Bound<'_, Self>, event: OrderExpired) {}
2442
2443 #[allow(unused_variables)]
2444 #[pyo3(name = "on_order_triggered")]
2445 fn py_on_order_triggered(_slf: &Bound<'_, Self>, event: OrderTriggered) {}
2446
2447 #[allow(unused_variables)]
2448 #[pyo3(name = "on_order_pending_update")]
2449 fn py_on_order_pending_update(_slf: &Bound<'_, Self>, event: OrderPendingUpdate) {}
2450
2451 #[allow(unused_variables)]
2452 #[pyo3(name = "on_order_pending_cancel")]
2453 fn py_on_order_pending_cancel(_slf: &Bound<'_, Self>, event: OrderPendingCancel) {}
2454
2455 #[allow(unused_variables)]
2456 #[pyo3(name = "on_order_modify_rejected")]
2457 fn py_on_order_modify_rejected(_slf: &Bound<'_, Self>, event: OrderModifyRejected) {}
2458
2459 #[allow(unused_variables)]
2460 #[pyo3(name = "on_order_cancel_rejected")]
2461 fn py_on_order_cancel_rejected(_slf: &Bound<'_, Self>, event: OrderCancelRejected) {}
2462
2463 #[allow(unused_variables)]
2464 #[pyo3(name = "on_order_updated")]
2465 fn py_on_order_updated(_slf: &Bound<'_, Self>, event: OrderUpdated) {}
2466
2467 #[allow(unused_variables)]
2468 #[pyo3(name = "on_order_canceled")]
2469 fn py_on_order_canceled(_slf: &Bound<'_, Self>, event: OrderCanceled) {}
2470
2471 #[allow(unused_variables, clippy::needless_pass_by_value)]
2472 #[pyo3(name = "on_order_filled")]
2473 fn py_on_order_filled(_slf: &Bound<'_, Self>, event: OrderFilled) {}
2474
2475 #[allow(unused_variables, clippy::needless_pass_by_value)]
2476 #[pyo3(name = "on_order_fill_voided")]
2477 fn py_on_order_fill_voided(_slf: &Bound<'_, Self>, event: OrderFillVoided) {}
2478
2479 #[allow(unused_variables, clippy::needless_pass_by_value)]
2480 #[pyo3(name = "on_position_opened")]
2481 fn py_on_position_opened(_slf: &Bound<'_, Self>, event: PositionOpened) {}
2482
2483 #[allow(unused_variables, clippy::needless_pass_by_value)]
2484 #[pyo3(name = "on_position_event")]
2485 fn py_on_position_event(_slf: &Bound<'_, Self>, event: Py<PyAny>) {}
2486
2487 #[allow(unused_variables, clippy::needless_pass_by_value)]
2488 #[pyo3(name = "on_position_changed")]
2489 fn py_on_position_changed(_slf: &Bound<'_, Self>, event: PositionChanged) {}
2490
2491 #[allow(unused_variables, clippy::needless_pass_by_value)]
2492 #[pyo3(name = "on_position_closed")]
2493 fn py_on_position_closed(_slf: &Bound<'_, Self>, event: PositionClosed) {}
2494
2495 #[allow(unused_variables, clippy::needless_pass_by_value)]
2496 #[pyo3(name = "on_data_response")]
2497 fn py_on_data_response(_slf: &Bound<'_, Self>, response: Py<PyDataResponse>) {}
2498
2499 #[allow(unused_variables, clippy::needless_pass_by_value)]
2500 #[pyo3(name = "on_historical_data")]
2501 fn py_on_historical_data(_slf: &Bound<'_, Self>, data: Py<PyAny>) {
2502 }
2504
2505 #[allow(unused_variables, clippy::needless_pass_by_value)]
2506 #[pyo3(name = "on_historical_book_deltas")]
2507 fn py_on_historical_book_deltas(_slf: &Bound<'_, Self>, deltas: Vec<OrderBookDelta>) {}
2508
2509 #[allow(unused_variables, clippy::needless_pass_by_value)]
2510 #[pyo3(name = "on_historical_book_depth")]
2511 fn py_on_historical_book_depth(_slf: &Bound<'_, Self>, depths: Vec<OrderBookDepth>) {}
2512
2513 #[allow(unused_variables, clippy::needless_pass_by_value)]
2514 #[pyo3(name = "on_historical_quotes")]
2515 fn py_on_historical_quotes(_slf: &Bound<'_, Self>, quotes: Vec<QuoteTick>) {
2516 }
2518
2519 #[allow(unused_variables, clippy::needless_pass_by_value)]
2520 #[pyo3(name = "on_historical_trades")]
2521 fn py_on_historical_trades(_slf: &Bound<'_, Self>, trades: Vec<TradeTick>) {
2522 }
2524
2525 #[allow(unused_variables, clippy::needless_pass_by_value)]
2526 #[pyo3(name = "on_historical_funding_rates")]
2527 fn py_on_historical_funding_rates(
2528 _slf: &Bound<'_, Self>,
2529 funding_rates: Vec<FundingRateUpdate>,
2530 ) {
2531 }
2533
2534 #[allow(unused_variables, clippy::needless_pass_by_value)]
2535 #[pyo3(name = "on_historical_bars")]
2536 fn py_on_historical_bars(_slf: &Bound<'_, Self>, bars: Vec<Bar>) {
2537 }
2539
2540 #[allow(unused_variables, clippy::needless_pass_by_value)]
2541 #[pyo3(name = "on_historical_mark_prices")]
2542 fn py_on_historical_mark_prices(_slf: &Bound<'_, Self>, mark_prices: Vec<MarkPriceUpdate>) {
2543 }
2545
2546 #[allow(unused_variables, clippy::needless_pass_by_value)]
2547 #[pyo3(name = "on_historical_index_prices")]
2548 fn py_on_historical_index_prices(_slf: &Bound<'_, Self>, index_prices: Vec<IndexPriceUpdate>) {
2549 }
2551
2552 #[pyo3(name = "subscribe_data")]
2553 #[pyo3(signature = (data_type, client_id=None, params=None))]
2554 fn py_subscribe_data(
2555 &mut self,
2556 data_type: DataType,
2557 client_id: Option<ClientId>,
2558 params: Option<Py<PyDict>>,
2559 ) -> PyResult<()> {
2560 self.ensure_registered()?;
2561 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2562 match params {
2563 Some(dict) => from_pydict(py, &dict),
2564 None => Ok(None),
2565 }
2566 })?;
2567 DataActor::subscribe_data(self.inner_mut(), data_type, client_id, params_map);
2568 Ok(())
2569 }
2570
2571 #[pyo3(name = "subscribe_signal")]
2572 #[pyo3(signature = (name="", priority=None))]
2573 fn py_subscribe_signal(&mut self, name: &str, priority: Option<u32>) -> PyResult<()> {
2574 self.ensure_registered()?;
2575 DataActor::subscribe_signal(self.inner_mut(), name, priority);
2576 Ok(())
2577 }
2578
2579 #[pyo3(name = "subscribe_queue_state")]
2580 #[pyo3(signature = (channel=None, priority=None))]
2581 fn py_subscribe_queue_state(
2582 &mut self,
2583 channel: Option<SystemChannel>,
2584 priority: Option<u32>,
2585 ) -> PyResult<()> {
2586 self.ensure_registered()?;
2587 DataActor::subscribe_queue_state(self.inner_mut(), channel, priority);
2588 Ok(())
2589 }
2590
2591 #[pyo3(name = "subscribe_socket_state")]
2592 #[pyo3(signature = (client_id=None, endpoint=None, priority=None))]
2593 fn py_subscribe_socket_state(
2594 &mut self,
2595 client_id: Option<ClientId>,
2596 endpoint: Option<&str>,
2597 priority: Option<u32>,
2598 ) -> PyResult<()> {
2599 self.ensure_registered()?;
2600 DataActor::subscribe_socket_state(self.inner_mut(), client_id, endpoint, priority);
2601 Ok(())
2602 }
2603
2604 #[pyo3(name = "subscribe_instruments")]
2605 #[pyo3(signature = (venue, client_id=None, params=None))]
2606 fn py_subscribe_instruments(
2607 &mut self,
2608 venue: Venue,
2609 client_id: Option<ClientId>,
2610 params: Option<Py<PyDict>>,
2611 ) -> PyResult<()> {
2612 self.ensure_registered()?;
2613 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2614 match params {
2615 Some(dict) => from_pydict(py, &dict),
2616 None => Ok(None),
2617 }
2618 })?;
2619 DataActor::subscribe_instruments(self.inner_mut(), venue, client_id, params_map);
2620 Ok(())
2621 }
2622
2623 #[pyo3(name = "subscribe_instrument")]
2624 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2625 fn py_subscribe_instrument(
2626 &mut self,
2627 #[gen_stub(override_type(
2628 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2629 ))]
2630 instrument_id: &Bound<'_, PyAny>,
2631 client_id: Option<ClientId>,
2632 params: Option<Py<PyDict>>,
2633 ) -> PyResult<()> {
2634 self.ensure_registered()?;
2635 let instrument_id = extract_instrument_ids(instrument_id)?;
2636 ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
2637 .map_err(to_pyvalue_err)?;
2638 ensure_subscription_routes(
2639 &instrument_id,
2640 client_id.as_ref(),
2641 &DataActorNative::cache_ref(self.inner()),
2642 )
2643 .map_err(to_pyvalue_err)?;
2644
2645 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2646 match params {
2647 Some(dict) => from_pydict(py, &dict),
2648 None => Ok(None),
2649 }
2650 })?;
2651 DataActor::subscribe_instrument(self.inner_mut(), instrument_id, client_id, params_map);
2652 Ok(())
2653 }
2654
2655 #[pyo3(name = "subscribe_book_deltas")]
2656 #[pyo3(signature = (instrument_id, book_type, depth=None, client_id=None, managed=false, params=None))]
2657 fn py_subscribe_book_deltas(
2658 &mut self,
2659 #[gen_stub(override_type(
2660 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2661 ))]
2662 instrument_id: &Bound<'_, PyAny>,
2663 book_type: BookType,
2664 depth: Option<usize>,
2665 client_id: Option<ClientId>,
2666 managed: bool,
2667 params: Option<Py<PyDict>>,
2668 ) -> PyResult<()> {
2669 self.ensure_registered()?;
2670 let instrument_id = extract_instrument_ids(instrument_id)?;
2671 ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
2672 .map_err(to_pyvalue_err)?;
2673 ensure_subscription_routes(
2674 &instrument_id,
2675 client_id.as_ref(),
2676 &DataActorNative::cache_ref(self.inner()),
2677 )
2678 .map_err(to_pyvalue_err)?;
2679
2680 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2681 match params {
2682 Some(dict) => from_pydict(py, &dict),
2683 None => Ok(None),
2684 }
2685 })?;
2686 let depth = depth.and_then(NonZeroUsize::new);
2687 DataActor::subscribe_book_deltas(
2688 self.inner_mut(),
2689 instrument_id,
2690 book_type,
2691 depth,
2692 client_id,
2693 managed,
2694 params_map,
2695 );
2696 Ok(())
2697 }
2698
2699 #[pyo3(name = "subscribe_book_depth")]
2700 #[pyo3(signature = (instrument_id, book_type, depth=None, client_id=None, managed=false, params=None))]
2701 fn py_subscribe_book_depth(
2702 &mut self,
2703 #[gen_stub(override_type(
2704 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2705 ))]
2706 instrument_id: &Bound<'_, PyAny>,
2707 book_type: BookType,
2708 depth: Option<usize>,
2709 client_id: Option<ClientId>,
2710 managed: bool,
2711 params: Option<Py<PyDict>>,
2712 ) -> PyResult<()> {
2713 self.ensure_registered()?;
2714 let instrument_id = extract_instrument_ids(instrument_id)?;
2715 ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
2716 .map_err(to_pyvalue_err)?;
2717 ensure_subscription_routes(
2718 &instrument_id,
2719 client_id.as_ref(),
2720 &DataActorNative::cache_ref(self.inner()),
2721 )
2722 .map_err(to_pyvalue_err)?;
2723
2724 let depth = depth
2725 .map(|value| {
2726 NonZeroUsize::new(value).ok_or_else(|| to_pyvalue_err("depth must be positive"))
2727 })
2728 .transpose()?;
2729
2730 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2731 match params {
2732 Some(dict) => from_pydict(py, &dict),
2733 None => Ok(None),
2734 }
2735 })?;
2736 DataActor::subscribe_book_depth(
2737 self.inner_mut(),
2738 instrument_id,
2739 book_type,
2740 depth,
2741 client_id,
2742 managed,
2743 params_map,
2744 );
2745 Ok(())
2746 }
2747
2748 #[pyo3(name = "subscribe_book_at_interval")]
2749 #[pyo3(signature = (instrument_id, book_type, interval_ms, depth=None, client_id=None, params=None))]
2750 fn py_subscribe_book_at_interval(
2751 &mut self,
2752 #[gen_stub(override_type(
2753 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2754 ))]
2755 instrument_id: &Bound<'_, PyAny>,
2756 book_type: BookType,
2757 interval_ms: usize,
2758 depth: Option<usize>,
2759 client_id: Option<ClientId>,
2760 params: Option<Py<PyDict>>,
2761 ) -> PyResult<()> {
2762 let interval_ms = NonZeroUsize::new(interval_ms)
2763 .ok_or_else(|| to_pyvalue_err("interval_ms must be > 0"))?;
2764
2765 self.ensure_registered()?;
2766 let instrument_id = extract_instrument_ids(instrument_id)?;
2767 ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
2768 .map_err(to_pyvalue_err)?;
2769 ensure_subscription_routes(
2770 &instrument_id,
2771 client_id.as_ref(),
2772 &DataActorNative::cache_ref(self.inner()),
2773 )
2774 .map_err(to_pyvalue_err)?;
2775
2776 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2777 match params {
2778 Some(dict) => from_pydict(py, &dict),
2779 None => Ok(None),
2780 }
2781 })?;
2782 let depth = depth.and_then(NonZeroUsize::new);
2783 DataActor::subscribe_book_at_interval(
2784 self.inner_mut(),
2785 instrument_id,
2786 book_type,
2787 depth,
2788 interval_ms,
2789 client_id,
2790 params_map,
2791 );
2792 Ok(())
2793 }
2794
2795 #[pyo3(name = "subscribe_quotes")]
2796 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2797 fn py_subscribe_quotes(
2798 &mut self,
2799 #[gen_stub(override_type(
2800 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2801 ))]
2802 instrument_id: &Bound<'_, PyAny>,
2803 client_id: Option<ClientId>,
2804 params: Option<Py<PyDict>>,
2805 ) -> PyResult<()> {
2806 self.ensure_registered()?;
2807 let instrument_ids = extract_instrument_ids(instrument_id)?;
2808 ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())
2809 .map_err(to_pyvalue_err)?;
2810
2811 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2812 match params {
2813 Some(dict) => from_pydict(py, &dict),
2814 None => Ok(None),
2815 }
2816 })?;
2817
2818 quote_routing(
2819 &instrument_ids,
2820 client_id.as_ref(),
2821 params_map.as_ref(),
2822 &DataActorNative::cache_ref(self.inner()),
2823 )
2824 .map_err(to_pyvalue_err)?;
2825 DataActor::subscribe_quotes(self.inner_mut(), instrument_ids, client_id, params_map);
2826 Ok(())
2827 }
2828
2829 #[pyo3(name = "subscribe_trades")]
2830 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2831 fn py_subscribe_trades(
2832 &mut self,
2833 #[gen_stub(override_type(
2834 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2835 ))]
2836 instrument_id: &Bound<'_, PyAny>,
2837 client_id: Option<ClientId>,
2838 params: Option<Py<PyDict>>,
2839 ) -> PyResult<()> {
2840 self.ensure_registered()?;
2841 let instrument_ids = extract_instrument_ids(instrument_id)?;
2842 ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())
2843 .map_err(to_pyvalue_err)?;
2844
2845 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2846 match params {
2847 Some(dict) => from_pydict(py, &dict),
2848 None => Ok(None),
2849 }
2850 })?;
2851
2852 DataActor::subscribe_trades(self.inner_mut(), instrument_ids, client_id, params_map);
2853 Ok(())
2854 }
2855
2856 #[pyo3(name = "subscribe_bars")]
2857 #[pyo3(signature = (bar_type, client_id=None, params=None))]
2858 fn py_subscribe_bars(
2859 &mut self,
2860 #[gen_stub(override_type(type_repr = "model.BarType | typing.Sequence[model.BarType]"))]
2861 bar_type: &Bound<'_, PyAny>,
2862 client_id: Option<ClientId>,
2863 params: Option<Py<PyDict>>,
2864 ) -> PyResult<()> {
2865 self.ensure_registered()?;
2866 let bar_types = extract_bar_types(bar_type)?;
2867 ensure_routable_bar_types(&bar_types, client_id.is_some()).map_err(to_pyvalue_err)?;
2868
2869 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2870 match params {
2871 Some(dict) => from_pydict(py, &dict),
2872 None => Ok(None),
2873 }
2874 })?;
2875
2876 DataActor::subscribe_bars(self.inner_mut(), bar_types, client_id, params_map);
2877 Ok(())
2878 }
2879
2880 #[pyo3(name = "subscribe_mark_prices")]
2881 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2882 fn py_subscribe_mark_prices(
2883 &mut self,
2884 #[gen_stub(override_type(
2885 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2886 ))]
2887 instrument_id: &Bound<'_, PyAny>,
2888 client_id: Option<ClientId>,
2889 params: Option<Py<PyDict>>,
2890 ) -> PyResult<()> {
2891 self.ensure_registered()?;
2892 let instrument_id = extract_instrument_ids(instrument_id)?;
2893 ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
2894 .map_err(to_pyvalue_err)?;
2895
2896 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2897 match params {
2898 Some(dict) => from_pydict(py, &dict),
2899 None => Ok(None),
2900 }
2901 })?;
2902 DataActor::subscribe_mark_prices(self.inner_mut(), instrument_id, client_id, params_map);
2903 Ok(())
2904 }
2905
2906 #[pyo3(name = "subscribe_index_prices")]
2907 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2908 fn py_subscribe_index_prices(
2909 &mut self,
2910 #[gen_stub(override_type(
2911 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2912 ))]
2913 instrument_id: &Bound<'_, PyAny>,
2914 client_id: Option<ClientId>,
2915 params: Option<Py<PyDict>>,
2916 ) -> PyResult<()> {
2917 self.ensure_registered()?;
2918 let instrument_id = extract_instrument_ids(instrument_id)?;
2919 ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
2920 .map_err(to_pyvalue_err)?;
2921
2922 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2923 match params {
2924 Some(dict) => from_pydict(py, &dict),
2925 None => Ok(None),
2926 }
2927 })?;
2928 DataActor::subscribe_index_prices(self.inner_mut(), instrument_id, client_id, params_map);
2929 Ok(())
2930 }
2931
2932 #[pyo3(name = "subscribe_funding_rates")]
2933 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2934 fn py_subscribe_funding_rates(
2935 &mut self,
2936 #[gen_stub(override_type(
2937 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2938 ))]
2939 instrument_id: &Bound<'_, PyAny>,
2940 client_id: Option<ClientId>,
2941 params: Option<Py<PyDict>>,
2942 ) -> PyResult<()> {
2943 self.ensure_registered()?;
2944 let instrument_id = extract_instrument_ids(instrument_id)?;
2945 ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
2946 .map_err(to_pyvalue_err)?;
2947
2948 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2949 match params {
2950 Some(dict) => from_pydict(py, &dict),
2951 None => Ok(None),
2952 }
2953 })?;
2954 DataActor::subscribe_funding_rates(self.inner_mut(), instrument_id, client_id, params_map);
2955 Ok(())
2956 }
2957
2958 #[pyo3(name = "subscribe_option_greeks")]
2959 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2960 fn py_subscribe_option_greeks(
2961 &mut self,
2962 #[gen_stub(override_type(
2963 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2964 ))]
2965 instrument_id: &Bound<'_, PyAny>,
2966 client_id: Option<ClientId>,
2967 params: Option<Py<PyDict>>,
2968 ) -> PyResult<()> {
2969 self.ensure_registered()?;
2970 let instrument_id = extract_instrument_ids(instrument_id)?;
2971 ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
2972 .map_err(to_pyvalue_err)?;
2973 ensure_subscription_routes(
2974 &instrument_id,
2975 client_id.as_ref(),
2976 &DataActorNative::cache_ref(self.inner()),
2977 )
2978 .map_err(to_pyvalue_err)?;
2979
2980 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2981 match params {
2982 Some(dict) => from_pydict(py, &dict),
2983 None => Ok(None),
2984 }
2985 })?;
2986 DataActor::subscribe_option_greeks(self.inner_mut(), instrument_id, client_id, params_map);
2987 Ok(())
2988 }
2989
2990 #[pyo3(name = "subscribe_instrument_status")]
2991 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2992 fn py_subscribe_instrument_status(
2993 &mut self,
2994 #[gen_stub(override_type(
2995 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2996 ))]
2997 instrument_id: &Bound<'_, PyAny>,
2998 client_id: Option<ClientId>,
2999 params: Option<Py<PyDict>>,
3000 ) -> PyResult<()> {
3001 self.ensure_registered()?;
3002 let instrument_id = extract_instrument_ids(instrument_id)?;
3003 ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
3004 .map_err(to_pyvalue_err)?;
3005 ensure_subscription_routes(
3006 &instrument_id,
3007 client_id.as_ref(),
3008 &DataActorNative::cache_ref(self.inner()),
3009 )
3010 .map_err(to_pyvalue_err)?;
3011
3012 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3013 match params {
3014 Some(dict) => from_pydict(py, &dict),
3015 None => Ok(None),
3016 }
3017 })?;
3018 DataActor::subscribe_instrument_status(
3019 self.inner_mut(),
3020 instrument_id,
3021 client_id,
3022 params_map,
3023 );
3024 Ok(())
3025 }
3026
3027 #[pyo3(name = "subscribe_instrument_close")]
3028 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
3029 fn py_subscribe_instrument_close(
3030 &mut self,
3031 #[gen_stub(override_type(
3032 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
3033 ))]
3034 instrument_id: &Bound<'_, PyAny>,
3035 client_id: Option<ClientId>,
3036 params: Option<Py<PyDict>>,
3037 ) -> PyResult<()> {
3038 self.ensure_registered()?;
3039 let instrument_id = extract_instrument_ids(instrument_id)?;
3040 ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
3041 .map_err(to_pyvalue_err)?;
3042 ensure_subscription_routes(
3043 &instrument_id,
3044 client_id.as_ref(),
3045 &DataActorNative::cache_ref(self.inner()),
3046 )
3047 .map_err(to_pyvalue_err)?;
3048
3049 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3050 match params {
3051 Some(dict) => from_pydict(py, &dict),
3052 None => Ok(None),
3053 }
3054 })?;
3055 DataActor::subscribe_instrument_close(
3056 self.inner_mut(),
3057 instrument_id,
3058 client_id,
3059 params_map,
3060 );
3061 Ok(())
3062 }
3063
3064 #[pyo3(name = "subscribe_option_chain")]
3065 #[pyo3(signature = (series_id, strike_range, snapshot_interval_ms=None, client_id=None, params=None))]
3066 fn py_subscribe_option_chain(
3067 &mut self,
3068 py: Python<'_>,
3069 series_id: OptionSeriesId,
3070 strike_range: PyStrikeRange,
3071 snapshot_interval_ms: Option<u64>,
3072 client_id: Option<ClientId>,
3073 params: Option<Py<PyDict>>,
3074 ) -> PyResult<()> {
3075 self.ensure_registered()?;
3076 let params_map = match params {
3077 Some(dict) => from_pydict(py, &dict)?,
3078 None => None,
3079 };
3080 DataActor::subscribe_option_chain(
3081 self.inner_mut(),
3082 series_id,
3083 strike_range.inner,
3084 snapshot_interval_ms,
3085 client_id,
3086 params_map,
3087 );
3088 Ok(())
3089 }
3090
3091 #[pyo3(name = "unsubscribe_data")]
3092 #[pyo3(signature = (data_type, client_id=None, params=None))]
3093 fn py_unsubscribe_data(
3094 &mut self,
3095 data_type: DataType,
3096 client_id: Option<ClientId>,
3097 params: Option<Py<PyDict>>,
3098 ) -> PyResult<()> {
3099 self.ensure_registered()?;
3100 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3101 match params {
3102 Some(dict) => from_pydict(py, &dict),
3103 None => Ok(None),
3104 }
3105 })?;
3106 DataActor::unsubscribe_data(self.inner_mut(), data_type, client_id, params_map);
3107 Ok(())
3108 }
3109
3110 #[pyo3(name = "unsubscribe_signal")]
3111 fn py_unsubscribe_signal(&mut self, name: &str) -> PyResult<()> {
3112 self.ensure_registered()?;
3113 DataActor::unsubscribe_signal(self.inner_mut(), name);
3114 Ok(())
3115 }
3116
3117 #[pyo3(name = "unsubscribe_queue_state")]
3118 #[pyo3(signature = (channel=None))]
3119 fn py_unsubscribe_queue_state(&mut self, channel: Option<SystemChannel>) -> PyResult<()> {
3120 self.ensure_registered()?;
3121 DataActor::unsubscribe_queue_state(self.inner_mut(), channel);
3122 Ok(())
3123 }
3124
3125 #[pyo3(name = "unsubscribe_socket_state")]
3126 #[pyo3(signature = (client_id=None, endpoint=None))]
3127 fn py_unsubscribe_socket_state(
3128 &mut self,
3129 client_id: Option<ClientId>,
3130 endpoint: Option<&str>,
3131 ) -> PyResult<()> {
3132 self.ensure_registered()?;
3133 DataActor::unsubscribe_socket_state(self.inner_mut(), client_id, endpoint);
3134 Ok(())
3135 }
3136
3137 #[pyo3(name = "unsubscribe_instruments")]
3138 #[pyo3(signature = (venue, client_id=None, params=None))]
3139 fn py_unsubscribe_instruments(
3140 &mut self,
3141 venue: Venue,
3142 client_id: Option<ClientId>,
3143 params: Option<Py<PyDict>>,
3144 ) -> PyResult<()> {
3145 self.ensure_registered()?;
3146 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3147 match params {
3148 Some(dict) => from_pydict(py, &dict),
3149 None => Ok(None),
3150 }
3151 })?;
3152 DataActor::unsubscribe_instruments(self.inner_mut(), venue, client_id, params_map);
3153 Ok(())
3154 }
3155
3156 #[pyo3(name = "unsubscribe_instrument")]
3157 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
3158 fn py_unsubscribe_instrument(
3159 &mut self,
3160 #[gen_stub(override_type(
3161 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
3162 ))]
3163 instrument_id: &Bound<'_, PyAny>,
3164 client_id: Option<ClientId>,
3165 params: Option<Py<PyDict>>,
3166 ) -> PyResult<()> {
3167 self.ensure_registered()?;
3168 let instrument_id = extract_instrument_ids(instrument_id)?;
3169 ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
3170 .map_err(to_pyvalue_err)?;
3171
3172 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3173 match params {
3174 Some(dict) => from_pydict(py, &dict),
3175 None => Ok(None),
3176 }
3177 })?;
3178 DataActor::unsubscribe_instrument(self.inner_mut(), instrument_id, client_id, params_map);
3179 Ok(())
3180 }
3181
3182 #[pyo3(name = "unsubscribe_book_deltas")]
3183 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
3184 fn py_unsubscribe_book_deltas(
3185 &mut self,
3186 #[gen_stub(override_type(
3187 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
3188 ))]
3189 instrument_id: &Bound<'_, PyAny>,
3190 client_id: Option<ClientId>,
3191 params: Option<Py<PyDict>>,
3192 ) -> PyResult<()> {
3193 self.ensure_registered()?;
3194 let instrument_id = extract_instrument_ids(instrument_id)?;
3195 ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
3196 .map_err(to_pyvalue_err)?;
3197
3198 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3199 match params {
3200 Some(dict) => from_pydict(py, &dict),
3201 None => Ok(None),
3202 }
3203 })?;
3204 DataActor::unsubscribe_book_deltas(self.inner_mut(), instrument_id, client_id, params_map);
3205 Ok(())
3206 }
3207
3208 #[pyo3(name = "unsubscribe_book_depth")]
3209 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
3210 fn py_unsubscribe_book_depth(
3211 &mut self,
3212 #[gen_stub(override_type(
3213 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
3214 ))]
3215 instrument_id: &Bound<'_, PyAny>,
3216 client_id: Option<ClientId>,
3217 params: Option<Py<PyDict>>,
3218 ) -> PyResult<()> {
3219 self.ensure_registered()?;
3220 let instrument_id = extract_instrument_ids(instrument_id)?;
3221 ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
3222 .map_err(to_pyvalue_err)?;
3223
3224 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3225 match params {
3226 Some(dict) => from_pydict(py, &dict),
3227 None => Ok(None),
3228 }
3229 })?;
3230 DataActor::unsubscribe_book_depth(self.inner_mut(), instrument_id, client_id, params_map);
3231 Ok(())
3232 }
3233
3234 #[pyo3(name = "unsubscribe_book_at_interval")]
3235 #[pyo3(signature = (instrument_id, interval_ms, client_id=None, params=None))]
3236 fn py_unsubscribe_book_at_interval(
3237 &mut self,
3238 #[gen_stub(override_type(
3239 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
3240 ))]
3241 instrument_id: &Bound<'_, PyAny>,
3242 interval_ms: usize,
3243 client_id: Option<ClientId>,
3244 params: Option<Py<PyDict>>,
3245 ) -> PyResult<()> {
3246 let interval_ms = NonZeroUsize::new(interval_ms)
3247 .ok_or_else(|| to_pyvalue_err("interval_ms must be > 0"))?;
3248
3249 self.ensure_registered()?;
3250 let instrument_id = extract_instrument_ids(instrument_id)?;
3251 ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
3252 .map_err(to_pyvalue_err)?;
3253
3254 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3255 match params {
3256 Some(dict) => from_pydict(py, &dict),
3257 None => Ok(None),
3258 }
3259 })?;
3260 DataActor::unsubscribe_book_at_interval(
3261 self.inner_mut(),
3262 instrument_id,
3263 interval_ms,
3264 client_id,
3265 params_map,
3266 );
3267 Ok(())
3268 }
3269
3270 #[pyo3(name = "unsubscribe_quotes")]
3271 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
3272 fn py_unsubscribe_quotes(
3273 &mut self,
3274 #[gen_stub(override_type(
3275 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
3276 ))]
3277 instrument_id: &Bound<'_, PyAny>,
3278 client_id: Option<ClientId>,
3279 params: Option<Py<PyDict>>,
3280 ) -> PyResult<()> {
3281 self.ensure_registered()?;
3282 let instrument_ids = extract_instrument_ids(instrument_id)?;
3283 ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())
3284 .map_err(to_pyvalue_err)?;
3285
3286 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3287 match params {
3288 Some(dict) => from_pydict(py, &dict),
3289 None => Ok(None),
3290 }
3291 })?;
3292
3293 DataActor::unsubscribe_quotes(self.inner_mut(), instrument_ids, client_id, params_map);
3294 Ok(())
3295 }
3296
3297 #[pyo3(name = "unsubscribe_trades")]
3298 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
3299 fn py_unsubscribe_trades(
3300 &mut self,
3301 #[gen_stub(override_type(
3302 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
3303 ))]
3304 instrument_id: &Bound<'_, PyAny>,
3305 client_id: Option<ClientId>,
3306 params: Option<Py<PyDict>>,
3307 ) -> PyResult<()> {
3308 self.ensure_registered()?;
3309 let instrument_ids = extract_instrument_ids(instrument_id)?;
3310 ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())
3311 .map_err(to_pyvalue_err)?;
3312
3313 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3314 match params {
3315 Some(dict) => from_pydict(py, &dict),
3316 None => Ok(None),
3317 }
3318 })?;
3319
3320 DataActor::unsubscribe_trades(self.inner_mut(), instrument_ids, client_id, params_map);
3321 Ok(())
3322 }
3323
3324 #[pyo3(name = "unsubscribe_bars")]
3325 #[pyo3(signature = (bar_type, client_id=None, params=None))]
3326 fn py_unsubscribe_bars(
3327 &mut self,
3328 #[gen_stub(override_type(type_repr = "model.BarType | typing.Sequence[model.BarType]"))]
3329 bar_type: &Bound<'_, PyAny>,
3330 client_id: Option<ClientId>,
3331 params: Option<Py<PyDict>>,
3332 ) -> PyResult<()> {
3333 self.ensure_registered()?;
3334 let bar_types = extract_bar_types(bar_type)?;
3335 ensure_routable_bar_types(&bar_types, client_id.is_some()).map_err(to_pyvalue_err)?;
3336
3337 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3338 match params {
3339 Some(dict) => from_pydict(py, &dict),
3340 None => Ok(None),
3341 }
3342 })?;
3343
3344 DataActor::unsubscribe_bars(self.inner_mut(), bar_types, client_id, params_map);
3345 Ok(())
3346 }
3347
3348 #[pyo3(name = "unsubscribe_mark_prices")]
3349 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
3350 fn py_unsubscribe_mark_prices(
3351 &mut self,
3352 #[gen_stub(override_type(
3353 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
3354 ))]
3355 instrument_id: &Bound<'_, PyAny>,
3356 client_id: Option<ClientId>,
3357 params: Option<Py<PyDict>>,
3358 ) -> PyResult<()> {
3359 self.ensure_registered()?;
3360 let instrument_id = extract_instrument_ids(instrument_id)?;
3361 ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
3362 .map_err(to_pyvalue_err)?;
3363
3364 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3365 match params {
3366 Some(dict) => from_pydict(py, &dict),
3367 None => Ok(None),
3368 }
3369 })?;
3370 DataActor::unsubscribe_mark_prices(self.inner_mut(), instrument_id, client_id, params_map);
3371 Ok(())
3372 }
3373
3374 #[pyo3(name = "unsubscribe_index_prices")]
3375 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
3376 fn py_unsubscribe_index_prices(
3377 &mut self,
3378 #[gen_stub(override_type(
3379 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
3380 ))]
3381 instrument_id: &Bound<'_, PyAny>,
3382 client_id: Option<ClientId>,
3383 params: Option<Py<PyDict>>,
3384 ) -> PyResult<()> {
3385 self.ensure_registered()?;
3386 let instrument_id = extract_instrument_ids(instrument_id)?;
3387 ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
3388 .map_err(to_pyvalue_err)?;
3389
3390 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3391 match params {
3392 Some(dict) => from_pydict(py, &dict),
3393 None => Ok(None),
3394 }
3395 })?;
3396 DataActor::unsubscribe_index_prices(self.inner_mut(), instrument_id, client_id, params_map);
3397 Ok(())
3398 }
3399
3400 #[pyo3(name = "unsubscribe_funding_rates")]
3401 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
3402 fn py_unsubscribe_funding_rates(
3403 &mut self,
3404 #[gen_stub(override_type(
3405 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
3406 ))]
3407 instrument_id: &Bound<'_, PyAny>,
3408 client_id: Option<ClientId>,
3409 params: Option<Py<PyDict>>,
3410 ) -> PyResult<()> {
3411 self.ensure_registered()?;
3412 let instrument_id = extract_instrument_ids(instrument_id)?;
3413 ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
3414 .map_err(to_pyvalue_err)?;
3415
3416 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3417 match params {
3418 Some(dict) => from_pydict(py, &dict),
3419 None => Ok(None),
3420 }
3421 })?;
3422 DataActor::unsubscribe_funding_rates(
3423 self.inner_mut(),
3424 instrument_id,
3425 client_id,
3426 params_map,
3427 );
3428 Ok(())
3429 }
3430
3431 #[pyo3(name = "unsubscribe_option_greeks")]
3432 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
3433 fn py_unsubscribe_option_greeks(
3434 &mut self,
3435 #[gen_stub(override_type(
3436 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
3437 ))]
3438 instrument_id: &Bound<'_, PyAny>,
3439 client_id: Option<ClientId>,
3440 params: Option<Py<PyDict>>,
3441 ) -> PyResult<()> {
3442 self.ensure_registered()?;
3443 let instrument_id = extract_instrument_ids(instrument_id)?;
3444 ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
3445 .map_err(to_pyvalue_err)?;
3446
3447 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3448 match params {
3449 Some(dict) => from_pydict(py, &dict),
3450 None => Ok(None),
3451 }
3452 })?;
3453 DataActor::unsubscribe_option_greeks(
3454 self.inner_mut(),
3455 instrument_id,
3456 client_id,
3457 params_map,
3458 );
3459 Ok(())
3460 }
3461
3462 #[pyo3(name = "unsubscribe_instrument_status")]
3463 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
3464 fn py_unsubscribe_instrument_status(
3465 &mut self,
3466 #[gen_stub(override_type(
3467 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
3468 ))]
3469 instrument_id: &Bound<'_, PyAny>,
3470 client_id: Option<ClientId>,
3471 params: Option<Py<PyDict>>,
3472 ) -> PyResult<()> {
3473 self.ensure_registered()?;
3474 let instrument_id = extract_instrument_ids(instrument_id)?;
3475 ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
3476 .map_err(to_pyvalue_err)?;
3477
3478 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3479 match params {
3480 Some(dict) => from_pydict(py, &dict),
3481 None => Ok(None),
3482 }
3483 })?;
3484 DataActor::unsubscribe_instrument_status(
3485 self.inner_mut(),
3486 instrument_id,
3487 client_id,
3488 params_map,
3489 );
3490 Ok(())
3491 }
3492
3493 #[pyo3(name = "unsubscribe_instrument_close")]
3494 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
3495 fn py_unsubscribe_instrument_close(
3496 &mut self,
3497 #[gen_stub(override_type(
3498 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
3499 ))]
3500 instrument_id: &Bound<'_, PyAny>,
3501 client_id: Option<ClientId>,
3502 params: Option<Py<PyDict>>,
3503 ) -> PyResult<()> {
3504 self.ensure_registered()?;
3505 let instrument_id = extract_instrument_ids(instrument_id)?;
3506 ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
3507 .map_err(to_pyvalue_err)?;
3508
3509 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3510 match params {
3511 Some(dict) => from_pydict(py, &dict),
3512 None => Ok(None),
3513 }
3514 })?;
3515 DataActor::unsubscribe_instrument_close(
3516 self.inner_mut(),
3517 instrument_id,
3518 client_id,
3519 params_map,
3520 );
3521 Ok(())
3522 }
3523
3524 #[pyo3(name = "unsubscribe_option_chain")]
3525 #[pyo3(signature = (series_id, client_id=None))]
3526 fn py_unsubscribe_option_chain(
3527 &mut self,
3528 series_id: OptionSeriesId,
3529 client_id: Option<ClientId>,
3530 ) -> PyResult<()> {
3531 self.ensure_registered()?;
3532 DataActor::unsubscribe_option_chain(self.inner_mut(), series_id, client_id);
3533 Ok(())
3534 }
3535
3536 #[pyo3(name = "request_data")]
3537 #[pyo3(signature = (data_type, client_id, start=None, end=None, limit=None, params=None, callback=None))]
3538 #[expect(clippy::too_many_arguments)]
3539 fn py_request_data(
3540 slf: &Bound<'_, Self>,
3541 py: Python<'_>,
3542 data_type: DataType,
3543 client_id: ClientId,
3544 start: Option<Timestamp>,
3545 end: Option<Timestamp>,
3546 limit: Option<usize>,
3547 params: Option<Py<PyDict>>,
3548 #[gen_stub(override_type(type_repr = "typing.Callable[[str], None] | None"))]
3549 callback: Option<Py<PyAny>>,
3550 ) -> PyResult<String> {
3551 let actor = Self {
3552 inner: Rc::clone(&slf.try_borrow()?.inner),
3553 };
3554
3555 actor.ensure_registered_for_data()?;
3556
3557 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3558 match params {
3559 Some(dict) => from_pydict(py, &dict),
3560 None => Ok(None),
3561 }
3562 })?;
3563 let limit = limit.and_then(NonZeroUsize::new);
3564 let handler = nautilus_common::python::response::request_handler(
3565 py,
3566 actor.inner().core().actor_id().inner(),
3567 PyStrategyInner::python_instance,
3568 callback,
3569 )?;
3570 let request_id = DataActorCore::request_data(
3571 actor.inner().core(),
3572 data_type,
3573 client_id,
3574 start,
3575 end,
3576 limit,
3577 params_map,
3578 handler,
3579 )
3580 .map_err(to_pyvalue_err)?;
3581 Ok(request_id.to_string())
3582 }
3583
3584 #[pyo3(name = "request_instrument")]
3585 #[pyo3(signature = (instrument_id, start=None, end=None, client_id=None, params=None, callback=None))]
3586 #[expect(clippy::too_many_arguments)]
3587 fn py_request_instrument(
3588 slf: &Bound<'_, Self>,
3589 py: Python<'_>,
3590 #[gen_stub(override_type(
3591 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
3592 ))]
3593 instrument_id: &Bound<'_, PyAny>,
3594 start: Option<Timestamp>,
3595 end: Option<Timestamp>,
3596 client_id: Option<ClientId>,
3597 params: Option<Py<PyDict>>,
3598 #[gen_stub(override_type(type_repr = "typing.Callable[[str], None] | None"))]
3599 callback: Option<Py<PyAny>>,
3600 ) -> PyResult<String> {
3601 let actor = Self {
3602 inner: Rc::clone(&slf.try_borrow()?.inner),
3603 };
3604
3605 let instrument_id = extract_instrument_ids(instrument_id)?;
3606 actor.ensure_registered_for_data()?;
3607
3608 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3609 match params {
3610 Some(dict) => from_pydict(py, &dict),
3611 None => Ok(None),
3612 }
3613 })?;
3614
3615 let handler = nautilus_common::python::response::request_handler(
3616 py,
3617 actor.inner().core().actor_id().inner(),
3618 PyStrategyInner::python_instance,
3619 callback,
3620 )?;
3621 let request_id = DataActorCore::request_instrument(
3622 actor.inner().core(),
3623 instrument_id,
3624 start,
3625 end,
3626 client_id,
3627 params_map,
3628 handler,
3629 )
3630 .map_err(to_pyvalue_err)?;
3631 Ok(request_id.to_string())
3632 }
3633
3634 #[pyo3(name = "request_instruments")]
3635 #[pyo3(signature = (venue=None, start=None, end=None, client_id=None, params=None, callback=None))]
3636 #[expect(clippy::too_many_arguments)]
3637 fn py_request_instruments(
3638 slf: &Bound<'_, Self>,
3639 py: Python<'_>,
3640 venue: Option<Venue>,
3641 start: Option<Timestamp>,
3642 end: Option<Timestamp>,
3643 client_id: Option<ClientId>,
3644 params: Option<Py<PyDict>>,
3645 #[gen_stub(override_type(type_repr = "typing.Callable[[str], None] | None"))]
3646 callback: Option<Py<PyAny>>,
3647 ) -> PyResult<String> {
3648 let actor = Self {
3649 inner: Rc::clone(&slf.try_borrow()?.inner),
3650 };
3651
3652 actor.ensure_registered_for_data()?;
3653
3654 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3655 match params {
3656 Some(dict) => from_pydict(py, &dict),
3657 None => Ok(None),
3658 }
3659 })?;
3660
3661 let handler = nautilus_common::python::response::request_handler(
3662 py,
3663 actor.inner().core().actor_id().inner(),
3664 PyStrategyInner::python_instance,
3665 callback,
3666 )?;
3667 let request_id = DataActorCore::request_instruments(
3668 actor.inner().core(),
3669 venue,
3670 start,
3671 end,
3672 client_id,
3673 params_map,
3674 handler,
3675 )
3676 .map_err(to_pyvalue_err)?;
3677 Ok(request_id.to_string())
3678 }
3679
3680 #[pyo3(name = "request_book_snapshot")]
3681 #[pyo3(signature = (instrument_id, depth=None, client_id=None, params=None, callback=None))]
3682 fn py_request_book_snapshot(
3683 slf: &Bound<'_, Self>,
3684 py: Python<'_>,
3685 #[gen_stub(override_type(
3686 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
3687 ))]
3688 instrument_id: &Bound<'_, PyAny>,
3689 depth: Option<usize>,
3690 client_id: Option<ClientId>,
3691 params: Option<Py<PyDict>>,
3692 #[gen_stub(override_type(type_repr = "typing.Callable[[str], None] | None"))]
3693 callback: Option<Py<PyAny>>,
3694 ) -> PyResult<String> {
3695 let actor = Self {
3696 inner: Rc::clone(&slf.try_borrow()?.inner),
3697 };
3698
3699 let instrument_id = extract_instrument_ids(instrument_id)?;
3700 actor.ensure_registered_for_data()?;
3701
3702 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3703 match params {
3704 Some(dict) => from_pydict(py, &dict),
3705 None => Ok(None),
3706 }
3707 })?;
3708 let depth = depth.and_then(NonZeroUsize::new);
3709
3710 let handler = nautilus_common::python::response::request_handler(
3711 py,
3712 actor.inner().core().actor_id().inner(),
3713 PyStrategyInner::python_instance,
3714 callback,
3715 )?;
3716 let request_id = DataActorCore::request_book_snapshot(
3717 actor.inner().core(),
3718 instrument_id,
3719 depth,
3720 client_id,
3721 params_map,
3722 handler,
3723 )
3724 .map_err(to_pyvalue_err)?;
3725 Ok(request_id.to_string())
3726 }
3727
3728 #[pyo3(name = "request_book_deltas")]
3729 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None, callback=None))]
3730 #[expect(clippy::too_many_arguments)]
3731 fn py_request_book_deltas(
3732 slf: &Bound<'_, Self>,
3733 py: Python<'_>,
3734 #[gen_stub(override_type(
3735 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
3736 ))]
3737 instrument_id: &Bound<'_, PyAny>,
3738 start: Option<Timestamp>,
3739 end: Option<Timestamp>,
3740 limit: Option<usize>,
3741 client_id: Option<ClientId>,
3742 params: Option<Py<PyDict>>,
3743 #[gen_stub(override_type(type_repr = "typing.Callable[[str], None] | None"))]
3744 callback: Option<Py<PyAny>>,
3745 ) -> PyResult<String> {
3746 let actor = Self {
3747 inner: Rc::clone(&slf.try_borrow()?.inner),
3748 };
3749
3750 let instrument_id = extract_instrument_ids(instrument_id)?;
3751 actor.ensure_registered_for_data()?;
3752
3753 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3754 match params {
3755 Some(dict) => from_pydict(py, &dict),
3756 None => Ok(None),
3757 }
3758 })?;
3759 let limit = limit.and_then(NonZeroUsize::new);
3760 let handler = nautilus_common::python::response::request_handler(
3761 py,
3762 actor.inner().core().actor_id().inner(),
3763 PyStrategyInner::python_instance,
3764 callback,
3765 )?;
3766 let request_id = DataActorCore::request_book_deltas(
3767 actor.inner().core(),
3768 instrument_id,
3769 start,
3770 end,
3771 limit,
3772 client_id,
3773 params_map,
3774 handler,
3775 )
3776 .map_err(to_pyvalue_err)?;
3777 Ok(request_id.to_string())
3778 }
3779
3780 #[pyo3(name = "request_book_depth")]
3781 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, depth=None, client_id=None, params=None, callback=None))]
3782 #[expect(clippy::too_many_arguments)]
3783 fn py_request_book_depth(
3784 slf: &Bound<'_, Self>,
3785 py: Python<'_>,
3786 #[gen_stub(override_type(
3787 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
3788 ))]
3789 instrument_id: &Bound<'_, PyAny>,
3790 start: Option<Timestamp>,
3791 end: Option<Timestamp>,
3792 limit: Option<usize>,
3793 depth: Option<usize>,
3794 client_id: Option<ClientId>,
3795 params: Option<Py<PyDict>>,
3796 #[gen_stub(override_type(type_repr = "typing.Callable[[str], None] | None"))]
3797 callback: Option<Py<PyAny>>,
3798 ) -> PyResult<String> {
3799 let actor = Self {
3800 inner: Rc::clone(&slf.try_borrow()?.inner),
3801 };
3802
3803 let instrument_id = extract_instrument_ids(instrument_id)?;
3804 actor.ensure_registered_for_data()?;
3805
3806 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3807 match params {
3808 Some(dict) => from_pydict(py, &dict),
3809 None => Ok(None),
3810 }
3811 })?;
3812 let limit = limit.and_then(NonZeroUsize::new);
3813 let depth = depth.and_then(NonZeroUsize::new);
3814 let handler = nautilus_common::python::response::request_handler(
3815 py,
3816 actor.inner().core().actor_id().inner(),
3817 PyStrategyInner::python_instance,
3818 callback,
3819 )?;
3820 let request_id = DataActorCore::request_book_depth(
3821 actor.inner().core(),
3822 instrument_id,
3823 start,
3824 end,
3825 limit,
3826 depth,
3827 client_id,
3828 params_map,
3829 handler,
3830 )
3831 .map_err(to_pyvalue_err)?;
3832 Ok(request_id.to_string())
3833 }
3834
3835 #[pyo3(name = "request_quotes")]
3836 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None, callback=None))]
3837 #[expect(clippy::too_many_arguments)]
3838 fn py_request_quotes(
3839 slf: &Bound<'_, Self>,
3840 py: Python<'_>,
3841 #[gen_stub(override_type(
3842 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
3843 ))]
3844 instrument_id: &Bound<'_, PyAny>,
3845 start: Option<Timestamp>,
3846 end: Option<Timestamp>,
3847 limit: Option<usize>,
3848 client_id: Option<ClientId>,
3849 params: Option<Py<PyDict>>,
3850 #[gen_stub(override_type(type_repr = "typing.Callable[[str], None] | None"))]
3851 callback: Option<Py<PyAny>>,
3852 ) -> PyResult<String> {
3853 let actor = Self {
3854 inner: Rc::clone(&slf.try_borrow()?.inner),
3855 };
3856
3857 let instrument_id = extract_instrument_ids(instrument_id)?;
3858 actor.ensure_registered_for_data()?;
3859
3860 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3861 match params {
3862 Some(dict) => from_pydict(py, &dict),
3863 None => Ok(None),
3864 }
3865 })?;
3866 let limit = limit.and_then(NonZeroUsize::new);
3867 let handler = nautilus_common::python::response::request_handler(
3868 py,
3869 actor.inner().core().actor_id().inner(),
3870 PyStrategyInner::python_instance,
3871 callback,
3872 )?;
3873 let request_id = DataActorCore::request_quotes(
3874 actor.inner().core(),
3875 instrument_id,
3876 start,
3877 end,
3878 limit,
3879 client_id,
3880 params_map,
3881 handler,
3882 )
3883 .map_err(to_pyvalue_err)?;
3884 Ok(request_id.to_string())
3885 }
3886
3887 #[pyo3(name = "request_trades")]
3888 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None, callback=None))]
3889 #[expect(clippy::too_many_arguments)]
3890 fn py_request_trades(
3891 slf: &Bound<'_, Self>,
3892 py: Python<'_>,
3893 #[gen_stub(override_type(
3894 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
3895 ))]
3896 instrument_id: &Bound<'_, PyAny>,
3897 start: Option<Timestamp>,
3898 end: Option<Timestamp>,
3899 limit: Option<usize>,
3900 client_id: Option<ClientId>,
3901 params: Option<Py<PyDict>>,
3902 #[gen_stub(override_type(type_repr = "typing.Callable[[str], None] | None"))]
3903 callback: Option<Py<PyAny>>,
3904 ) -> PyResult<String> {
3905 let actor = Self {
3906 inner: Rc::clone(&slf.try_borrow()?.inner),
3907 };
3908
3909 let instrument_id = extract_instrument_ids(instrument_id)?;
3910 actor.ensure_registered_for_data()?;
3911
3912 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3913 match params {
3914 Some(dict) => from_pydict(py, &dict),
3915 None => Ok(None),
3916 }
3917 })?;
3918 let limit = limit.and_then(NonZeroUsize::new);
3919 let handler = nautilus_common::python::response::request_handler(
3920 py,
3921 actor.inner().core().actor_id().inner(),
3922 PyStrategyInner::python_instance,
3923 callback,
3924 )?;
3925 let request_id = DataActorCore::request_trades(
3926 actor.inner().core(),
3927 instrument_id,
3928 start,
3929 end,
3930 limit,
3931 client_id,
3932 params_map,
3933 handler,
3934 )
3935 .map_err(to_pyvalue_err)?;
3936 Ok(request_id.to_string())
3937 }
3938
3939 #[pyo3(name = "request_funding_rates")]
3940 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None, callback=None))]
3941 #[expect(clippy::too_many_arguments)]
3942 fn py_request_funding_rates(
3943 slf: &Bound<'_, Self>,
3944 py: Python<'_>,
3945 #[gen_stub(override_type(
3946 type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
3947 ))]
3948 instrument_id: &Bound<'_, PyAny>,
3949 start: Option<Timestamp>,
3950 end: Option<Timestamp>,
3951 limit: Option<usize>,
3952 client_id: Option<ClientId>,
3953 params: Option<Py<PyDict>>,
3954 #[gen_stub(override_type(type_repr = "typing.Callable[[str], None] | None"))]
3955 callback: Option<Py<PyAny>>,
3956 ) -> PyResult<String> {
3957 let actor = Self {
3958 inner: Rc::clone(&slf.try_borrow()?.inner),
3959 };
3960
3961 let instrument_id = extract_instrument_ids(instrument_id)?;
3962 actor.ensure_registered_for_data()?;
3963
3964 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3965 match params {
3966 Some(dict) => from_pydict(py, &dict),
3967 None => Ok(None),
3968 }
3969 })?;
3970 let limit = limit.and_then(NonZeroUsize::new);
3971 let handler = nautilus_common::python::response::request_handler(
3972 py,
3973 actor.inner().core().actor_id().inner(),
3974 PyStrategyInner::python_instance,
3975 callback,
3976 )?;
3977 let request_id = DataActorCore::request_funding_rates(
3978 actor.inner().core(),
3979 instrument_id,
3980 start,
3981 end,
3982 limit,
3983 client_id,
3984 params_map,
3985 handler,
3986 )
3987 .map_err(to_pyvalue_err)?;
3988 Ok(request_id.to_string())
3989 }
3990
3991 #[pyo3(name = "request_bars")]
3992 #[pyo3(signature = (bar_type, start=None, end=None, limit=None, client_id=None, params=None, callback=None))]
3993 #[expect(clippy::too_many_arguments)]
3994 fn py_request_bars(
3995 slf: &Bound<'_, Self>,
3996 py: Python<'_>,
3997 #[gen_stub(override_type(type_repr = "model.BarType | typing.Sequence[model.BarType]"))]
3998 bar_type: &Bound<'_, PyAny>,
3999 start: Option<Timestamp>,
4000 end: Option<Timestamp>,
4001 limit: Option<usize>,
4002 client_id: Option<ClientId>,
4003 params: Option<Py<PyDict>>,
4004 #[gen_stub(override_type(type_repr = "typing.Callable[[str], None] | None"))]
4005 callback: Option<Py<PyAny>>,
4006 ) -> PyResult<String> {
4007 let actor = Self {
4008 inner: Rc::clone(&slf.try_borrow()?.inner),
4009 };
4010
4011 let bar_type = extract_bar_types(bar_type)?;
4012 actor.ensure_registered_for_data()?;
4013
4014 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
4015 match params {
4016 Some(dict) => from_pydict(py, &dict),
4017 None => Ok(None),
4018 }
4019 })?;
4020 let limit = limit.and_then(NonZeroUsize::new);
4021 let handler = nautilus_common::python::response::request_handler(
4022 py,
4023 actor.inner().core().actor_id().inner(),
4024 PyStrategyInner::python_instance,
4025 callback,
4026 )?;
4027 let request_id = DataActorCore::request_bars(
4028 actor.inner().core(),
4029 bar_type,
4030 start,
4031 end,
4032 limit,
4033 client_id,
4034 params_map,
4035 handler,
4036 )
4037 .map_err(to_pyvalue_err)?;
4038 Ok(request_id.to_string())
4039 }
4040
4041 #[pyo3(name = "request_join", signature = (request_ids, callback=None, request_id=None))]
4042 fn py_request_join(
4043 slf: &Bound<'_, Self>,
4044 py: Python<'_>,
4045 #[gen_stub(override_type(type_repr = "typing.Sequence[str | core.UUID4]", imports = ("typing", "nautilus_trader.core")))]
4046 request_ids: Vec<Bound<'_, PyAny>>,
4047 #[gen_stub(override_type(type_repr = "typing.Callable[[str], None] | None"))]
4048 callback: Option<Py<PyAny>>,
4049 #[gen_stub(override_type(type_repr = "str | core.UUID4 | None", imports = ("nautilus_trader.core",)))]
4050 request_id: Option<&Bound<'_, PyAny>>,
4051 ) -> PyResult<String> {
4052 let actor_id = {
4053 let actor = slf.try_borrow()?;
4054 actor.ensure_registered_for_data()?;
4055 actor.inner().core().actor_id().inner()
4056 };
4057
4058 let request_ids = request_ids
4059 .into_iter()
4060 .map(|id| nautilus_common::python::response::extract_request_id(&id))
4061 .collect::<PyResult<Vec<_>>>()?;
4062 let request_id = request_id
4063 .map(nautilus_common::python::response::extract_request_id)
4064 .transpose()?;
4065 nautilus_common::python::response::request_join::<PyStrategyInner>(
4066 py,
4067 actor_id,
4068 PyStrategyInner::python_instance,
4069 request_ids,
4070 request_id,
4071 callback,
4072 )
4073 }
4074
4075 #[pyo3(name = "is_pending_request")]
4076 fn py_is_pending_request(
4077 &self,
4078 #[gen_stub(override_type(type_repr = "str | core.UUID4", imports = ("nautilus_trader.core",)))]
4079 request_id: &Bound<'_, PyAny>,
4080 ) -> PyResult<bool> {
4081 let id = nautilus_common::python::response::extract_request_id(request_id)?;
4082 Ok(self.inner().core().is_pending_request(&id))
4083 }
4084
4085 #[pyo3(name = "has_pending_requests")]
4086 fn py_has_pending_requests(&self) -> bool {
4087 !self.inner().core().pending_requests().is_empty()
4088 }
4089
4090 #[pyo3(name = "pending_requests")]
4091 #[gen_stub(override_return_type(type_repr = "set[str]"))]
4092 fn py_pending_requests(&self, py: Python<'_>) -> PyResult<Py<PySet>> {
4093 let ids = self.inner().core().pending_requests();
4094 PySet::new(py, ids.iter().map(ToString::to_string)).map(Bound::unbind)
4095 }
4096
4097 #[pyo3(name = "reconnect_socket")]
4099 fn py_reconnect_socket(&self, client_id: ClientId, endpoint: &str) -> PyResult<()> {
4100 DataActor::reconnect_socket(self.inner(), client_id, endpoint).map_err(to_pyruntime_err)
4101 }
4102}
4103
4104#[pyo3_stub_gen::derive::gen_stub_pymethods]
4105#[pyo3::pymethods]
4106impl PyStrategy {
4107 #[pyo3(name = "publish_message", signature = (topic, message))]
4108 fn py_publish_message(
4109 slf: &Bound<'_, Self>,
4110 topic: &str,
4111 #[gen_stub(override_type(type_repr = "object"))] message: Py<PyAny>,
4112 ) -> PyResult<()> {
4113 let messages = get_python_message_bus(slf.as_any())?;
4114 messages.publish_message(topic, message)
4115 }
4116
4117 #[pyo3(name = "subscribe_topic")]
4118 #[pyo3(signature = (topic, handler, priority=0))]
4119 fn py_subscribe_topic(
4120 slf: &Bound<'_, Self>,
4121 topic: &str,
4122 #[gen_stub(override_type(type_repr = "collections.abc.Callable[[object], None]", imports = ("collections.abc",)))]
4123 handler: Py<PyAny>,
4124 priority: u32,
4125 ) -> PyResult<()> {
4126 let messages = get_python_message_bus(slf.as_any())?;
4127 messages.subscribe_topic(slf.py(), topic, handler, priority)
4128 }
4129
4130 #[pyo3(name = "unsubscribe_topic", signature = (topic, handler))]
4131 fn py_unsubscribe_topic(
4132 slf: &Bound<'_, Self>,
4133 topic: &str,
4134 #[gen_stub(override_type(type_repr = "collections.abc.Callable[[object], None]", imports = ("collections.abc",)))]
4135 handler: &Bound<'_, PyAny>,
4136 ) -> PyResult<()> {
4137 let messages = get_python_message_bus(slf.as_any())?;
4138 messages.unsubscribe_topic(topic, handler)
4139 }
4140}
4141
4142impl PyStrategy {
4143 fn ensure_registered_for_data(&self) -> PyResult<()> {
4144 if self.inner().core.actor.is_registered() {
4145 Ok(())
4146 } else {
4147 Err(to_pyruntime_err(
4148 "Strategy must be registered before publishing, managing synthetics, or requesting data",
4149 ))
4150 }
4151 }
4152
4153 fn ensure_registered(&self) -> PyResult<()> {
4154 if self.inner().core.actor.is_registered() {
4155 Ok(())
4156 } else {
4157 Err(to_pyruntime_err(
4158 "Strategy must be registered before managing subscriptions",
4159 ))
4160 }
4161 }
4162}
4163
4164fn class_derived_actor_id(slf: &Bound<'_, PyStrategy>, class_name: &str) -> PyResult<ActorId> {
4169 let borrowed = slf.borrow();
4170 let order_id_tag = normalize_order_id_tag(borrowed.inner().core.order_id_tag())
4171 .unwrap_or(UNASSIGNED_ORDER_ID_TAG);
4172
4173 ActorId::new_checked(format!("{class_name}-{order_id_tag}")).map_err(to_pyvalue_err)
4174}
4175
4176fn has_configured_strategy_id(slf: &Bound<'_, PyStrategy>) -> bool {
4182 let py = slf.py();
4183 let config = slf
4184 .borrow()
4185 .inner()
4186 .config
4187 .as_ref()
4188 .map(|config| config.clone_ref(py));
4189
4190 config.is_some_and(|config| {
4191 config
4192 .bind(py)
4193 .getattr("strategy_id")
4194 .is_ok_and(|strategy_id| !strategy_id.is_none())
4195 })
4196}
4197
4198fn py_order_list_to_orders(py: Python<'_>, order_list: &Py<PyAny>) -> PyResult<Vec<OrderAny>> {
4199 let order_objects = match order_list.getattr(py, "orders") {
4200 Ok(orders) => orders.extract::<Vec<Py<PyAny>>>(py)?,
4201 Err(e) if e.is_instance_of::<pyo3::exceptions::PyAttributeError>(py) => {
4202 order_list.extract::<Vec<Py<PyAny>>>(py)?
4203 }
4204 Err(e) => return Err(e),
4205 };
4206
4207 order_objects
4208 .into_iter()
4209 .map(|order| pyobject_to_order_any(py, order))
4210 .collect()
4211}
4212
4213#[cfg(test)]
4214mod tests {
4215 use std::{
4216 cell::RefCell,
4217 collections::{BTreeMap, HashMap},
4218 num::NonZeroUsize,
4219 rc::Rc,
4220 str::FromStr,
4221 sync::Arc,
4222 };
4223
4224 use indexmap::IndexMap;
4225 use nautilus_common::{
4226 actor::{DataActor, DataActorNative, registry::actor_exists},
4227 cache::Cache,
4228 clock::{Clock, VirtualClock},
4229 component::{Component, get_component},
4230 enums::ComponentState,
4231 live::runner::replace_system_command_sender,
4232 logging::{
4233 arm_shutdown_on_error, disarm_shutdown_on_error, init_logging,
4234 take_shutdown_on_error_trigger,
4235 },
4236 messages::{
4237 SystemCommand,
4238 data::{
4239 BarsResponse, DataCommand, QuotesResponse, SubscribeCommand, TradesResponse,
4240 UnsubscribeCommand,
4241 },
4242 execution::TradingCommand,
4243 system::{
4244 QueueCondition, QueueState, QueueStateChanged, SocketState, SocketStateChanged,
4245 },
4246 },
4247 msgbus::{
4248 self, MessagingSwitchboard,
4249 stubs::{TypedIntoMessageSavingHandler, get_typed_into_message_saving_handler},
4250 },
4251 python::{cache::PyCache, wrappers::get_python_wrapper},
4252 runner::{SyncDataCommandSender, SystemChannel, set_data_cmd_sender},
4253 signal::Signal,
4254 timer::TimeEvent,
4255 };
4256 use nautilus_core::{DurationNanos, UUID4, UnixNanos};
4257 use nautilus_model::{
4258 data::{
4259 Bar, BarType, CustomData, FundingRateUpdate, IndexPriceUpdate, InstrumentStatus,
4260 MarkPriceUpdate, OrderBookDelta, OrderBookDeltas, OrderBookDepth, QuoteTick, TradeTick,
4261 close::InstrumentClose,
4262 greeks::OptionGreekValues,
4263 option_chain::{OptionChainSlice, OptionGreeks},
4264 stubs::{stub_custom_data, stub_deltas, stub_depth10},
4265 },
4266 enums::{
4267 AggressorSide, BookType, GreeksConvention, InstrumentCloseType, MarketStatusAction,
4268 OmsType, OrderSide, OrderStatus, OrderType, PositionSide, TimeInForce,
4269 },
4270 events::{
4271 OrderAccepted, OrderCancelRejected, OrderCanceled, OrderDenied, OrderEmulated,
4272 OrderEventAny, OrderExpired, OrderInitialized, OrderModifyRejected, OrderPendingCancel,
4273 OrderPendingUpdate, OrderRejected, OrderReleased, OrderSubmitted, OrderTriggered,
4274 OrderUpdated, PositionChanged, PositionClosed, PositionEvent, PositionOpened,
4275 order::spec::{OrderFillVoidedSpec, OrderFilledSpec},
4276 },
4277 identifiers::{
4278 AccountId, ClientId, ClientOrderId, ComponentId, InstrumentId, OptionSeriesId,
4279 OrderListId, PositionId, StrategyId, TradeId, TraderId, Venue,
4280 },
4281 instruments::{CurrencyPair, InstrumentAny, stubs::audusd_sim},
4282 orderbook::OrderBook,
4283 orders::{Order, OrderTestBuilder},
4284 position::Position,
4285 python::orders::order_any_to_pyobject,
4286 types::{Currency, Money, Price, Quantity},
4287 };
4288 use nautilus_portfolio::portfolio::Portfolio;
4289 use pyo3::{
4290 Bound, IntoPyObjectExt, Py, PyAny, PyResult, Python,
4291 ffi::c_str,
4292 types::{PyAnyMethods, PyBytes, PyDict, PyList, PyWeakrefMethods, PyWeakrefReference},
4293 };
4294 use serde_json::Value;
4295 use ustr::Ustr;
4296
4297 use super::PyStrategy;
4298 use crate::strategy::{Strategy, StrategyConfig};
4299
4300 const TRACKING_STRATEGY_CODE: &std::ffi::CStr = c_str!(
4301 r#"
4302class TrackingStrategy:
4303 TRACKED_METHODS = {
4304 "on_start",
4305 "on_stop",
4306 "on_resume",
4307 "on_reset",
4308 "on_dispose",
4309 "on_degrade",
4310 "on_fault",
4311 "on_save",
4312 "on_load",
4313 "on_time_event",
4314 "on_data",
4315 "on_signal",
4316 "on_queue_state",
4317 "on_socket_state",
4318 "on_instrument",
4319 "on_quote",
4320 "on_trade",
4321 "on_bar",
4322 "on_book_deltas",
4323 "on_book_depth",
4324 "on_book",
4325 "on_mark_price",
4326 "on_index_price",
4327 "on_funding_rate",
4328 "on_instrument_status",
4329 "on_instrument_close",
4330 "on_option_greeks",
4331 "on_option_chain",
4332 "on_historical_data",
4333 "on_historical_book_deltas",
4334 "on_historical_book_depth",
4335 "on_historical_quotes",
4336 "on_historical_trades",
4337 "on_historical_funding_rates",
4338 "on_historical_bars",
4339 "on_historical_mark_prices",
4340 "on_historical_index_prices",
4341 "on_market_exit",
4342 "post_market_exit",
4343 "on_order_initialized",
4344 "on_order_event",
4345 "on_order_denied",
4346 "on_order_emulated",
4347 "on_order_released",
4348 "on_order_submitted",
4349 "on_order_rejected",
4350 "on_order_accepted",
4351 "on_order_expired",
4352 "on_order_triggered",
4353 "on_order_pending_update",
4354 "on_order_pending_cancel",
4355 "on_order_modify_rejected",
4356 "on_order_cancel_rejected",
4357 "on_order_updated",
4358 "on_order_canceled",
4359 "on_order_filled",
4360 "on_order_fill_voided",
4361 "on_position_opened",
4362 "on_position_event",
4363 "on_position_changed",
4364 "on_position_closed",
4365 }
4366
4367 def __init__(self):
4368 self.calls = []
4369 self.raise_on = None
4370
4371 def _record(self, method_name, *args):
4372 self.calls.append((method_name, args))
4373 if self.raise_on == method_name:
4374 self.fail_callback()
4375
4376 def fail_callback(self):
4377 try:
4378 raise ValueError("callback cause")
4379 except ValueError as e:
4380 raise RuntimeError("callback failure") from e
4381
4382 def was_called(self, method_name):
4383 return any(call[0] == method_name for call in self.calls)
4384
4385 def call_count(self, method_name):
4386 return sum(1 for call in self.calls if call[0] == method_name)
4387
4388 def last_call_args(self, method_name):
4389 for called_method, args in reversed(self.calls):
4390 if called_method == method_name:
4391 return args
4392 raise AssertionError(f"{method_name} was not called")
4393
4394 def call_names(self):
4395 return [call[0] for call in self.calls]
4396
4397 def last_loaded_state(self):
4398
4399 for method_name, args in reversed(self.calls):
4400 if method_name == "on_load":
4401 return args[0]
4402 return None
4403
4404 def on_save(self):
4405 self._record("on_save")
4406 return {"strategy": b"saved"}
4407
4408 def on_load(self, state):
4409 self._record("on_load", dict(state))
4410
4411 def __getattr__(self, name):
4412 if name in self.TRACKED_METHODS:
4413 return lambda *args: self._record(name, *args)
4414 raise AttributeError(name)
4415"#
4416 );
4417
4418 fn create_tracking_python_strategy(py: Python<'_>) -> PyResult<Py<PyAny>> {
4419 py.run(TRACKING_STRATEGY_CODE, None, None)?;
4420 let tracking_strategy_class = py.eval(c_str!("TrackingStrategy"), None, None)?;
4421 let instance = tracking_strategy_class.call0()?;
4422 Ok(instance.unbind())
4423 }
4424
4425 fn python_method_was_called(
4426 py_strategy: &Py<PyAny>,
4427 py: Python<'_>,
4428 method_name: &str,
4429 ) -> bool {
4430 py_strategy
4431 .call_method1(py, "was_called", (method_name,))
4432 .and_then(|result| result.extract::<bool>(py))
4433 .unwrap_or(false)
4434 }
4435
4436 fn python_method_call_count(py_strategy: &Py<PyAny>, py: Python<'_>, method_name: &str) -> i32 {
4437 py_strategy
4438 .call_method1(py, "call_count", (method_name,))
4439 .and_then(|result| result.extract::<i32>(py))
4440 .unwrap_or(0)
4441 }
4442
4443 fn python_method_call_names(py_strategy: &Py<PyAny>, py: Python<'_>) -> Vec<String> {
4444 py_strategy
4445 .call_method0(py, "call_names")
4446 .and_then(|result| result.extract::<Vec<String>>(py))
4447 .unwrap_or_default()
4448 }
4449
4450 fn python_last_loaded_state(
4451 py_strategy: &Py<PyAny>,
4452 py: Python<'_>,
4453 ) -> Option<HashMap<String, Vec<u8>>> {
4454 py_strategy
4455 .call_method0(py, "last_loaded_state")
4456 .and_then(|result| result.extract::<Option<HashMap<String, Vec<u8>>>>(py))
4457 .unwrap_or(None)
4458 }
4459
4460 const TRACKING_INDICATOR_CODE: &std::ffi::CStr = c_str!(
4461 r#"
4462class TrackingIndicator:
4463 def __init__(self, events=None):
4464 self.initialized = False
4465 self.calls = []
4466 self.events = events
4467
4468 def handle_quote_tick(self, quote):
4469 self.calls.append("quote")
4470 if self.events is not None:
4471 self.events.append("indicator:quote")
4472
4473 def handle_trade_tick(self, trade):
4474 self.calls.append("trade")
4475 if self.events is not None:
4476 self.events.append("indicator:trade")
4477
4478 def handle_bar(self, bar):
4479 self.calls.append("bar")
4480 if self.events is not None:
4481 self.events.append("indicator:bar")
4482
4483 def call_count(self, name):
4484 return self.calls.count(name)
4485
4486class IndicatorEventStrategy:
4487 def __init__(self, events):
4488 self.events = events
4489
4490 def on_start(self):
4491 pass
4492
4493 def on_quote(self, quote):
4494 self.events.append("strategy:quote")
4495
4496 def on_trade(self, trade):
4497 self.events.append("strategy:trade")
4498
4499 def on_bar(self, bar):
4500 self.events.append("strategy:bar")
4501"#
4502 );
4503
4504 fn create_tracking_python_indicator(py: Python<'_>) -> PyResult<Py<PyAny>> {
4505 py.run(TRACKING_INDICATOR_CODE, None, None)?;
4506 let indicator_class = py.eval(c_str!("TrackingIndicator"), None, None)?;
4507 Ok(indicator_class.call0()?.unbind())
4508 }
4509
4510 fn create_event_tracking_python_indicator(
4511 py: Python<'_>,
4512 events: &Bound<'_, PyList>,
4513 ) -> PyResult<Py<PyAny>> {
4514 py.run(TRACKING_INDICATOR_CODE, None, None)?;
4515 let indicator_class = py.eval(c_str!("TrackingIndicator"), None, None)?;
4516 Ok(indicator_class.call1((events,))?.unbind())
4517 }
4518
4519 fn create_indicator_event_strategy(
4520 py: Python<'_>,
4521 events: &Bound<'_, PyList>,
4522 ) -> PyResult<Py<PyAny>> {
4523 py.run(TRACKING_INDICATOR_CODE, None, None)?;
4524 let strategy_class = py.eval(c_str!("IndicatorEventStrategy"), None, None)?;
4525 Ok(strategy_class.call1((events,))?.unbind())
4526 }
4527
4528 fn python_indicator_call_count(
4529 indicator: &Py<PyAny>,
4530 py: Python<'_>,
4531 method_name: &str,
4532 ) -> i32 {
4533 indicator
4534 .call_method1(py, "call_count", (method_name,))
4535 .and_then(|result| result.extract::<i32>(py))
4536 .unwrap_or(0)
4537 }
4538
4539 fn sample_instrument() -> CurrencyPair {
4540 audusd_sim()
4541 }
4542
4543 fn sample_time_event() -> TimeEvent {
4544 TimeEvent::new(
4545 Ustr::from("test_timer"),
4546 UUID4::new(),
4547 UnixNanos::default(),
4548 UnixNanos::default(),
4549 )
4550 }
4551
4552 fn sample_data() -> CustomData {
4553 stub_custom_data(1, 42, None, None)
4554 }
4555
4556 fn sample_signal() -> Signal {
4557 Signal::new(
4558 Ustr::from("test_signal"),
4559 "1.0".to_string(),
4560 UnixNanos::default(),
4561 UnixNanos::default(),
4562 )
4563 }
4564
4565 fn sample_queue_state_changed() -> QueueStateChanged {
4566 QueueStateChanged::new(
4567 TraderId::from("TRADER-001"),
4568 SystemChannel::ExecCommands,
4569 QueueCondition::Backlogged,
4570 QueueState::Triggered,
4571 17,
4572 23,
4573 UUID4::from("00000000-0000-4000-8000-000000000001"),
4574 UnixNanos::from(1_700_000_000_000_000_001),
4575 UnixNanos::from(1_700_000_000_000_000_002),
4576 )
4577 }
4578
4579 fn sample_socket_state_changed() -> SocketStateChanged {
4580 SocketStateChanged::new(
4581 TraderId::from("TRADER-001"),
4582 ClientId::from("BINANCE"),
4583 Some(Venue::from("BINANCE")),
4584 Ustr::from("binance-futures-market-streams"),
4585 SocketState::Connected,
4586 UUID4::from("00000000-0000-4000-8000-000000000001"),
4587 UnixNanos::from(1_700_000_000_000_000_001),
4588 UnixNanos::from(1_700_000_000_000_000_002),
4589 )
4590 }
4591
4592 fn sample_quote() -> QuoteTick {
4593 let instrument = sample_instrument();
4594 QuoteTick::new(
4595 instrument.id,
4596 Price::from("1.00000"),
4597 Price::from("1.00001"),
4598 Quantity::from(100_000),
4599 Quantity::from(100_000),
4600 UnixNanos::default(),
4601 UnixNanos::default(),
4602 )
4603 }
4604
4605 fn sample_trade() -> TradeTick {
4606 let instrument = sample_instrument();
4607 TradeTick::new(
4608 instrument.id,
4609 Price::from("1.00000"),
4610 Quantity::from(100_000),
4611 AggressorSide::Buy,
4612 TradeId::new("123456"),
4613 UnixNanos::default(),
4614 UnixNanos::default(),
4615 )
4616 }
4617
4618 fn sample_bar() -> Bar {
4619 let instrument = sample_instrument();
4620 let bar_type =
4621 BarType::from_str(&format!("{}-1-MINUTE-LAST-INTERNAL", instrument.id)).unwrap();
4622 Bar::new(
4623 bar_type,
4624 Price::from("1.00000"),
4625 Price::from("1.00010"),
4626 Price::from("0.99990"),
4627 Price::from("1.00005"),
4628 Quantity::from(100_000),
4629 UnixNanos::default(),
4630 UnixNanos::default(),
4631 )
4632 }
4633
4634 fn sample_book() -> OrderBook {
4635 OrderBook::new(sample_instrument().id, BookType::L2_MBP)
4636 }
4637
4638 fn sample_book_deltas() -> OrderBookDeltas {
4639 let instrument = sample_instrument();
4640 let delta =
4641 OrderBookDelta::clear(instrument.id, 0, UnixNanos::default(), UnixNanos::default());
4642 OrderBookDeltas::new(instrument.id, vec![delta])
4643 }
4644
4645 fn sample_book_depth() -> OrderBookDepth {
4646 stub_depth10()
4647 }
4648
4649 fn sample_mark_price() -> MarkPriceUpdate {
4650 MarkPriceUpdate::new(
4651 sample_instrument().id,
4652 Price::from("1.00000"),
4653 UnixNanos::default(),
4654 UnixNanos::default(),
4655 )
4656 }
4657
4658 fn sample_index_price() -> IndexPriceUpdate {
4659 IndexPriceUpdate::new(
4660 sample_instrument().id,
4661 Price::from("1.00000"),
4662 UnixNanos::default(),
4663 UnixNanos::default(),
4664 )
4665 }
4666
4667 fn sample_funding_rate() -> FundingRateUpdate {
4668 FundingRateUpdate::new(
4669 sample_instrument().id,
4670 "0.0001".parse().unwrap(),
4671 None,
4672 None,
4673 UnixNanos::default(),
4674 UnixNanos::default(),
4675 )
4676 }
4677
4678 fn sample_instrument_status() -> InstrumentStatus {
4679 InstrumentStatus::new(
4680 sample_instrument().id,
4681 MarketStatusAction::Trading,
4682 UnixNanos::default(),
4683 UnixNanos::default(),
4684 None,
4685 None,
4686 None,
4687 None,
4688 None,
4689 )
4690 }
4691
4692 fn sample_instrument_close() -> InstrumentClose {
4693 InstrumentClose::new(
4694 sample_instrument().id,
4695 Price::from("1.00000"),
4696 InstrumentCloseType::EndOfSession,
4697 UnixNanos::default(),
4698 UnixNanos::default(),
4699 )
4700 }
4701
4702 fn sample_option_greeks() -> OptionGreeks {
4703 OptionGreeks {
4704 instrument_id: InstrumentId::from("AUD/USD.SIM"),
4705 convention: GreeksConvention::BlackScholes,
4706 greeks: OptionGreekValues {
4707 delta: 0.55,
4708 gamma: 0.03,
4709 vega: 0.12,
4710 theta: -0.05,
4711 rho: 0.01,
4712 },
4713 mark_iv: Some(0.25),
4714 bid_iv: None,
4715 ask_iv: None,
4716 underlying_price: None,
4717 open_interest: None,
4718 ts_event: UnixNanos::default(),
4719 ts_init: UnixNanos::default(),
4720 }
4721 }
4722
4723 fn sample_option_chain() -> OptionChainSlice {
4724 OptionChainSlice {
4725 series_id: OptionSeriesId::new_derived(
4726 Venue::from("SIM"),
4727 Ustr::from("AUD"),
4728 Ustr::from("USD"),
4729 UnixNanos::from(1_711_036_800_000_000_000),
4730 ),
4731 atm_strike: None,
4732 calls: BTreeMap::default(),
4733 puts: BTreeMap::default(),
4734 ts_event: UnixNanos::default(),
4735 ts_init: UnixNanos::default(),
4736 }
4737 }
4738
4739 fn sample_position_opened() -> PositionOpened {
4740 PositionOpened {
4741 trader_id: TraderId::from("TRADER-001"),
4742 strategy_id: StrategyId::from("TEST-001"),
4743 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
4744 position_id: PositionId::from("P-001"),
4745 account_id: AccountId::from("ACC-001"),
4746 opening_order_id: ClientOrderId::from("O-001"),
4747 entry: OrderSide::Buy,
4748 side: PositionSide::Long,
4749 signed_qty: 1.into(),
4750 quantity: Quantity::from(1),
4751 last_qty: Quantity::from(1),
4752 last_px: Price::from("1.00000"),
4753 currency: Currency::from("USD"),
4754 avg_px_open: 1.into(),
4755 realized_pnl: None,
4756 event_id: UUID4::new(),
4757 ts_event: UnixNanos::default(),
4758 ts_init: UnixNanos::default(),
4759 }
4760 }
4761
4762 fn sample_position_changed() -> PositionChanged {
4763 PositionChanged {
4764 trader_id: TraderId::from("TRADER-001"),
4765 strategy_id: StrategyId::from("TEST-001"),
4766 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
4767 position_id: PositionId::from("P-001"),
4768 account_id: AccountId::from("ACC-001"),
4769 opening_order_id: ClientOrderId::from("O-001"),
4770 entry: OrderSide::Buy,
4771 side: PositionSide::Long,
4772 signed_qty: 2.into(),
4773 quantity: Quantity::from(2),
4774 peak_quantity: Quantity::from(2),
4775 last_qty: Quantity::from(1),
4776 last_px: Price::from("1.10000"),
4777 currency: Currency::from("USD"),
4778 avg_px_open: "1.05".parse().unwrap(),
4779 avg_px_close: None,
4780 realized_return: 0.0,
4781 realized_pnl: None,
4782 unrealized_pnl: Money::new(0.0, Currency::USD()),
4783 event_id: UUID4::new(),
4784 ts_opened: UnixNanos::default(),
4785 ts_event: UnixNanos::default(),
4786 ts_init: UnixNanos::default(),
4787 }
4788 }
4789
4790 fn sample_position_closed() -> PositionClosed {
4791 PositionClosed {
4792 trader_id: TraderId::from("TRADER-001"),
4793 strategy_id: StrategyId::from("TEST-001"),
4794 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
4795 position_id: PositionId::from("P-001"),
4796 account_id: AccountId::from("ACC-001"),
4797 opening_order_id: ClientOrderId::from("O-001"),
4798 closing_order_id: Some(ClientOrderId::from("O-002")),
4799 entry: OrderSide::Buy,
4800 side: PositionSide::Flat,
4801 signed_qty: 0.into(),
4802 quantity: Quantity::from(0),
4803 peak_quantity: Quantity::from(2),
4804 last_qty: Quantity::from(2),
4805 last_px: Price::from("1.20000"),
4806 currency: Currency::from("USD"),
4807 avg_px_open: "1.05".parse().unwrap(),
4808 avg_px_close: Some("1.20".parse().unwrap()),
4809 realized_return: 0.1,
4810 realized_pnl: Some(Money::new(0.1, Currency::USD())),
4811 unrealized_pnl: Money::new(0.0, Currency::USD()),
4812 duration: DurationNanos::new(1),
4813 event_id: UUID4::new(),
4814 ts_opened: UnixNanos::default(),
4815 ts_closed: Some(UnixNanos::default()),
4816 ts_event: UnixNanos::default(),
4817 ts_init: UnixNanos::default(),
4818 }
4819 }
4820
4821 fn sample_python_market_order(
4822 py: Python<'_>,
4823 strategy_id: StrategyId,
4824 client_order_id: ClientOrderId,
4825 ) -> PyResult<Py<PyAny>> {
4826 let order = OrderTestBuilder::new(OrderType::Market)
4827 .trader_id(TraderId::from("TRADER-001"))
4828 .strategy_id(strategy_id)
4829 .instrument_id(sample_instrument().id)
4830 .client_order_id(client_order_id)
4831 .quantity(Quantity::from(100_000))
4832 .build();
4833
4834 order_any_to_pyobject(py, order)
4835 }
4836
4837 fn sample_open_position(
4838 strategy_id: StrategyId,
4839 position_id: PositionId,
4840 client_order_id: ClientOrderId,
4841 ) -> Position {
4842 let instrument = sample_instrument();
4843 let fill = OrderFilledSpec::builder()
4844 .trader_id(TraderId::from("TRADER-001"))
4845 .strategy_id(strategy_id)
4846 .instrument_id(instrument.id)
4847 .client_order_id(client_order_id)
4848 .position_id(position_id)
4849 .order_side(OrderSide::Buy)
4850 .last_qty(Quantity::from(100_000))
4851 .last_px(Price::from("1.00000"))
4852 .currency(instrument.quote_currency)
4853 .build();
4854
4855 Position::new(&InstrumentAny::CurrencyPair(instrument), fill)
4856 }
4857
4858 fn create_registered_tracking_strategy_with_config(
4859 py: Python<'_>,
4860 config: Option<StrategyConfig>,
4861 ) -> (Py<PyAny>, PyStrategy) {
4862 let py_strategy = create_tracking_python_strategy(py).unwrap();
4863 let mut rust_strategy = PyStrategy::new(config);
4864 rust_strategy
4865 .set_python_instance(py_strategy.bind(py))
4866 .unwrap();
4867
4868 let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(VirtualClock::new()));
4869 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
4870 let portfolio = Rc::new(RefCell::new(Portfolio::new(
4871 clock.clone(),
4872 cache.clone(),
4873 None,
4874 )));
4875
4876 rust_strategy
4877 .register(TraderId::from("TRADER-001"), clock, cache, portfolio)
4878 .unwrap();
4879
4880 (py_strategy, rust_strategy)
4881 }
4882
4883 fn create_registered_tracking_strategy(py: Python<'_>) -> (Py<PyAny>, PyStrategy) {
4884 create_registered_tracking_strategy_with_config(py, None)
4885 }
4886
4887 #[rstest::rstest]
4888 fn test_python_strategy_identifier_lists_require_registration_before_validation(
4889 #[values(
4890 "subscribe_quotes",
4891 "unsubscribe_quotes",
4892 "subscribe_trades",
4893 "unsubscribe_trades"
4894 )]
4895 method: &str,
4896 #[values("empty", "invalid", "mixed_venues")] input: &str,
4897 ) {
4898 Python::initialize();
4899 Python::attach(|py| {
4900 let strategy = PyStrategy::new(None);
4901 let strategy = Py::new(py, strategy).unwrap();
4902 let argument = match input {
4903 "empty" => PyList::empty(py).into_any(),
4904 "invalid" => PyList::new(py, [42]).unwrap().into_any(),
4905 "mixed_venues" => PyList::new(
4906 py,
4907 [
4908 InstrumentId::from("BTC/USD.SIM"),
4909 InstrumentId::from("ETH/USD.OTHER"),
4910 ],
4911 )
4912 .unwrap()
4913 .into_any(),
4914 _ => unreachable!(),
4915 };
4916
4917 let error = strategy
4918 .bind(py)
4919 .call_method1(method, (&argument,))
4920 .unwrap_err();
4921
4922 assert!(error.is_instance_of::<pyo3::exceptions::PyRuntimeError>(py));
4923 assert_eq!(
4924 error.value(py).to_string(),
4925 "Strategy must be registered before managing subscriptions"
4926 );
4927 });
4928 }
4929
4930 #[rstest::rstest]
4931 fn test_python_strategy_identifier_lists_reject_mixed_venues_before_effects(
4932 #[values(
4933 "subscribe_quotes",
4934 "unsubscribe_quotes",
4935 "subscribe_trades",
4936 "unsubscribe_trades"
4937 )]
4938 method: &str,
4939 ) {
4940 Python::initialize();
4941 Python::attach(|py| {
4942 let (_instance, strategy) = create_registered_tracking_strategy(py);
4943 let strategy = Py::new(py, strategy).unwrap();
4944 set_data_cmd_sender(Arc::new(SyncDataCommandSender));
4945 let (handler, saver) = get_typed_into_message_saving_handler::<DataCommand>(None);
4946 msgbus::register_data_command_endpoint(
4947 MessagingSwitchboard::data_engine_queue_execute(),
4948 handler,
4949 );
4950 let first = InstrumentId::from("BTC/USD.SIM");
4951 let second = InstrumentId::from("ETH/USD.OTHER");
4952 let ids = PyList::new(py, [first, second]).unwrap();
4953 let quotes = method.ends_with("quotes");
4954 let subscribe = method.starts_with("subscribe_");
4955 if !subscribe {
4956 let kwargs = PyDict::new(py);
4957 kwargs
4958 .set_item("client_id", ClientId::from("ORIGINAL"))
4959 .unwrap();
4960 strategy
4961 .bind(py)
4962 .call_method(
4963 if quotes {
4964 "subscribe_quotes"
4965 } else {
4966 "subscribe_trades"
4967 },
4968 (&ids,),
4969 Some(&kwargs),
4970 )
4971 .unwrap();
4972 }
4973
4974 let error = strategy.bind(py).call_method1(method, (&ids,)).unwrap_err();
4975
4976 assert!(error.is_instance_of::<pyo3::exceptions::PyValueError>(py));
4977 assert_eq!(
4978 error.value(py).to_string(),
4979 "multiple instrument venues require an explicit client ID"
4980 );
4981 assert_eq!(saver.get_messages().len(), usize::from(!subscribe));
4982
4983 for id in [first, second] {
4984 assert_eq!(
4985 msgbus::exact_subscriber_count_quotes(msgbus::switchboard::get_quotes_topic(
4986 id
4987 )),
4988 usize::from(!subscribe && quotes),
4989 );
4990 assert_eq!(
4991 msgbus::exact_subscriber_count_trades(msgbus::switchboard::get_trades_topic(
4992 id
4993 )),
4994 usize::from(!subscribe && !quotes),
4995 );
4996 }
4997 });
4998 }
4999
5000 #[rstest::rstest]
5001 fn test_python_subscription_lists_reject_invalid_routes_before_effects(
5002 #[values(
5003 "subscribe_quotes",
5004 "subscribe_instrument",
5005 "subscribe_book_deltas",
5006 "subscribe_book_depth",
5007 "subscribe_book_at_interval",
5008 "subscribe_option_greeks",
5009 "subscribe_instrument_status",
5010 "subscribe_instrument_close"
5011 )]
5012 method: &str,
5013 #[values(false, true)] external: bool,
5014 ) {
5015 Python::initialize();
5016 Python::attach(|py| {
5017 let client_id = ClientId::from("ROUTED");
5018 let (_instance, strategy) = create_registered_tracking_strategy(py);
5019 if external {
5020 DataActorNative::cache_rc(strategy.inner())
5021 .borrow_mut()
5022 .add_external_data_client(client_id);
5023 }
5024
5025 let actor = Py::new(py, strategy).unwrap();
5026 set_data_cmd_sender(Arc::new(SyncDataCommandSender));
5027 let (handler, saver) = get_typed_into_message_saving_handler::<DataCommand>(None);
5028 msgbus::register_data_command_endpoint(
5029 MessagingSwitchboard::data_engine_queue_execute(),
5030 handler,
5031 );
5032 let ids = PyList::new(
5033 py,
5034 [
5035 InstrumentId::from("AUD/USD.SIM"),
5036 InstrumentId::from("SYNTH.SYNTH"),
5037 ],
5038 )
5039 .unwrap();
5040 let kwargs = PyDict::new(py);
5041 kwargs.set_item("client_id", client_id).unwrap();
5042 if method.starts_with("subscribe_book") {
5043 kwargs.set_item("book_type", BookType::L2_MBP).unwrap();
5044 }
5045
5046 if method == "subscribe_book_at_interval" {
5047 kwargs.set_item("interval_ms", 13).unwrap();
5048 }
5049
5050 let result = actor.bind(py).call_method(method, (&ids,), Some(&kwargs));
5051 if external {
5052 result.unwrap();
5053 } else {
5054 let error = result.unwrap_err();
5055 assert!(error.is_instance_of::<pyo3::exceptions::PyValueError>(py));
5056
5057 let expected = if method == "subscribe_quotes" {
5058 "quote identifier lists cannot mix client, synthetic, and spread routes"
5059 } else {
5060 "Cannot subscribe synthetic instruments for this data type"
5061 };
5062
5063 assert_eq!(error.value(py).to_string(), expected);
5064 assert!(saver.get_messages().is_empty());
5065 }
5066
5067 if method.starts_with("subscribe_book") {
5068 kwargs.del_item("book_type").unwrap();
5069 }
5070
5071 let unsubscribe = method.replacen("subscribe", "unsubscribe", 1);
5072 actor
5073 .bind(py)
5074 .call_method(unsubscribe.as_str(), (&ids,), Some(&kwargs))
5075 .unwrap();
5076 assert_eq!(saver.get_messages().len(), if external { 2 } else { 0 });
5077 });
5078 }
5079
5080 #[rstest::rstest]
5081 #[case::quotes("quotes")]
5082 #[case::trades("trades")]
5083 fn test_python_strategy_identifier_lists(
5084 #[case] family: &str,
5085 #[values(false, true)] mixed_venues: bool,
5086 ) {
5087 pyo3::Python::initialize();
5088 Python::attach(|py| {
5089 let (_instance, strategy) = create_registered_tracking_strategy(py);
5090 let strategy = Py::new(py, strategy).unwrap();
5091 set_data_cmd_sender(Arc::new(SyncDataCommandSender));
5092 let (handler, saver) = get_typed_into_message_saving_handler::<DataCommand>(None);
5093 msgbus::register_data_command_endpoint(
5094 MessagingSwitchboard::data_engine_queue_execute(),
5095 handler,
5096 );
5097 let first = InstrumentId::from("BTC/USD.SIM");
5098
5099 let second = if mixed_venues {
5100 InstrumentId::from("ETH/USD.OTHER")
5101 } else {
5102 InstrumentId::from("ETH/USD.SIM")
5103 };
5104
5105 let client_id = mixed_venues.then(|| ClientId::from("EXPLICIT"));
5106 let ids = PyList::new(py, [second, first, second]).unwrap();
5107 let kwargs = PyDict::new(py);
5108 kwargs.set_item("instrument_id", &ids).unwrap();
5109 kwargs.set_item("client_id", client_id).unwrap();
5110
5111 for action in ["subscribe", "unsubscribe"] {
5112 strategy
5113 .bind(py)
5114 .call_method(format!("{action}_{family}").as_str(), (), Some(&kwargs))
5115 .unwrap();
5116 }
5117
5118 let commands = saver.get_messages();
5119
5120 let actual: Vec<_> = commands
5121 .iter()
5122 .map(|command| match command {
5123 DataCommand::Subscribe(SubscribeCommand::Quotes(cmd)) => (
5124 "quotes",
5125 true,
5126 cmd.instrument_ids.clone(),
5127 cmd.client_id,
5128 cmd.venue,
5129 cmd.params.clone(),
5130 ),
5131 DataCommand::Subscribe(SubscribeCommand::Trades(cmd)) => (
5132 "trades",
5133 true,
5134 cmd.instrument_ids.clone(),
5135 cmd.client_id,
5136 cmd.venue,
5137 cmd.params.clone(),
5138 ),
5139 DataCommand::Unsubscribe(UnsubscribeCommand::Quotes(cmd)) => (
5140 "quotes",
5141 false,
5142 cmd.instrument_ids.clone(),
5143 cmd.client_id,
5144 cmd.venue,
5145 cmd.params.clone(),
5146 ),
5147 DataCommand::Unsubscribe(UnsubscribeCommand::Trades(cmd)) => (
5148 "trades",
5149 false,
5150 cmd.instrument_ids.clone(),
5151 cmd.client_id,
5152 cmd.venue,
5153 cmd.params.clone(),
5154 ),
5155 _ => panic!("unexpected subscription command {command:?}"),
5156 })
5157 .collect();
5158
5159 let expected = if mixed_venues {
5160 vec![
5161 (family, true, vec![second, first], client_id, None, None),
5162 (family, false, vec![second, first], client_id, None, None),
5163 ]
5164 } else {
5165 vec![
5166 (
5167 family,
5168 true,
5169 vec![second, first],
5170 None,
5171 Some(first.venue),
5172 None,
5173 ),
5174 (
5175 family,
5176 false,
5177 vec![second, first],
5178 None,
5179 Some(first.venue),
5180 None,
5181 ),
5182 ]
5183 };
5184
5185 assert_eq!(actual, expected);
5186 });
5187 }
5188
5189 #[rstest::rstest]
5190 fn test_register_in_global_registries_retains_python_wrapper() {
5191 pyo3::Python::initialize();
5192
5193 Python::attach(|py| {
5194 let (py_strategy, rust_strategy) = create_registered_tracking_strategy_with_config(
5195 py,
5196 Some(StrategyConfig {
5197 strategy_id: Some(StrategyId::from("Retained-001")),
5198 ..Default::default()
5199 }),
5200 );
5201
5202 rust_strategy.register_in_global_registries().unwrap();
5203
5204 let retained = get_python_wrapper(ComponentId::from("Retained-001"))
5205 .expect("registering must retain the strategy's Python wrapper");
5206
5207 assert!(retained.bind(py).is(py_strategy.bind(py)));
5208 assert!(get_component(&Ustr::from("Retained-001")).is_some());
5209 assert!(actor_exists(&Ustr::from("Retained-001")));
5210 });
5211 }
5212
5213 #[rstest::rstest]
5214 fn test_registered_python_strategy_routes_time_events_by_state() {
5215 pyo3::Python::initialize();
5216
5217 Python::attach(|py| {
5218 let (py_strategy, rust_strategy) = create_registered_tracking_strategy_with_config(
5219 py,
5220 Some(StrategyConfig {
5221 strategy_id: Some(StrategyId::from("StoppedTimer-001")),
5222 manage_gtd_expiry: true,
5223 ..Default::default()
5224 }),
5225 );
5226 rust_strategy.register_in_global_registries().unwrap();
5227 Component::start(rust_strategy.inner_mut()).unwrap();
5228
5229 let running_client_order_id = ClientOrderId::from("O-RUNNING-001");
5230 let running_timer_name = format!("GTD-EXPIRY:{running_client_order_id}");
5231 let clock = DataActorNative::clock_rc(rust_strategy.inner());
5232 clock
5233 .borrow_mut()
5234 .set_time_alert_ns(&running_timer_name, UnixNanos::from(1), None, None)
5235 .unwrap();
5236 rust_strategy
5237 .inner_mut()
5238 .core
5239 .gtd_timers
5240 .insert(running_client_order_id, Ustr::from(&running_timer_name));
5241 let running_dispatched = dispatch_time_events(&clock, UnixNanos::from(1));
5242
5243 assert_eq!(running_dispatched, 1);
5244 assert!(
5245 !rust_strategy
5246 .inner_mut()
5247 .has_gtd_expiry_timer(&running_client_order_id)
5248 );
5249 assert_eq!(
5250 python_method_call_count(&py_strategy, py, "on_time_event"),
5251 1
5252 );
5253
5254 let client_order_id = ClientOrderId::from("O-STOPPED-001");
5255 let timer_name = format!("GTD-EXPIRY:{client_order_id}");
5256 clock
5257 .borrow_mut()
5258 .set_time_alert_ns(&timer_name, UnixNanos::from(2), None, None)
5259 .unwrap();
5260 rust_strategy
5261 .inner_mut()
5262 .core
5263 .gtd_timers
5264 .insert(client_order_id, Ustr::from(&timer_name));
5265 Component::stop(rust_strategy.inner_mut()).unwrap();
5266
5267 let stopped_dispatched = dispatch_time_events(&clock, UnixNanos::from(2));
5268
5269 assert_eq!(stopped_dispatched, 1);
5270 assert_eq!(
5271 Component::state(rust_strategy.inner()),
5272 ComponentState::Stopped
5273 );
5274 assert!(
5275 rust_strategy
5276 .inner_mut()
5277 .has_gtd_expiry_timer(&client_order_id)
5278 );
5279 assert_eq!(
5280 python_method_call_count(&py_strategy, py, "on_time_event"),
5281 1
5282 );
5283 });
5284 }
5285
5286 fn dispatch_time_events(clock: &Rc<RefCell<dyn Clock>>, to_time_ns: UnixNanos) -> usize {
5287 let handlers = {
5288 let mut clock_ref = clock.borrow_mut();
5289 let test_clock = clock_ref
5290 .as_any_mut()
5291 .downcast_mut::<VirtualClock>()
5292 .expect("strategy clock must be VirtualClock");
5293 let events = test_clock.advance_time(to_time_ns, true);
5294 test_clock.match_handlers(events)
5295 };
5296 let dispatched = handlers.len();
5297
5298 for handler in handlers {
5299 handler.run();
5300 }
5301
5302 dispatched
5303 }
5304
5305 #[rstest::rstest]
5306 fn test_register_in_global_registries_rejects_missing_python_wrapper() {
5307 pyo3::Python::initialize();
5308
5309 Python::attach(|_py| {
5310 let mut rust_strategy = PyStrategy::new(Some(StrategyConfig {
5311 strategy_id: Some(StrategyId::from("Unwrapped-001")),
5312 ..Default::default()
5313 }));
5314
5315 let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(VirtualClock::new()));
5316 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
5317 let portfolio = Rc::new(RefCell::new(Portfolio::new(
5318 clock.clone(),
5319 cache.clone(),
5320 None,
5321 )));
5322 rust_strategy
5323 .register(TraderId::from("TRADER-001"), clock, cache, portfolio)
5324 .unwrap();
5325
5326 let error = rust_strategy
5327 .register_in_global_registries()
5328 .expect_err("registering without a Python wrapper must fail");
5329
5330 assert!(error.to_string().contains("without a Python wrapper"));
5331 assert!(get_component(&Ustr::from("Unwrapped-001")).is_none());
5332 assert!(!actor_exists(&Ustr::from("Unwrapped-001")));
5333 assert!(get_python_wrapper(ComponentId::from("Unwrapped-001")).is_none());
5334 });
5335 }
5336
5337 #[rstest::rstest]
5338 fn test_external_order_instrument_ids_returns_configured_instruments() {
5339 let claims = vec![
5340 InstrumentId::from("AUDUSD.SIM"),
5341 InstrumentId::from("BTCUSDT.BINANCE"),
5342 ];
5343 let strategy = PyStrategy::new(Some(StrategyConfig {
5344 external_order_instrument_ids: Some(claims.clone()),
5345 ..Default::default()
5346 }));
5347
5348 assert_eq!(strategy.external_order_instrument_ids(), Some(claims));
5349 }
5350
5351 #[rstest::rstest]
5352 fn test_new_checked_rejects_order_id_tag_with_separator() {
5353 let config = StrategyConfig {
5354 order_id_tag: Some("A-B".to_string()),
5355 ..Default::default()
5356 };
5357
5358 let error = PyStrategy::new_checked(Some(config)).unwrap_err();
5359
5360 assert_eq!(
5361 error.to_string(),
5362 "`order_id_tag` cannot contain the '-' strategy ID separator, was 'A-B'"
5363 );
5364 }
5365
5366 #[rstest::rstest]
5367 fn test_py_new_raises_value_error_for_order_id_tag_with_separator() {
5368 pyo3::Python::initialize();
5369 Python::attach(|py| {
5370 let config = StrategyConfig {
5373 order_id_tag: Some("A-B".to_string()),
5374 ..Default::default()
5375 };
5376 let config_obj = Py::new(py, config).unwrap().into_any();
5377
5378 let error = PyStrategy::py_new(Some(config_obj)).unwrap_err();
5379
5380 assert!(error.is_instance_of::<pyo3::exceptions::PyValueError>(py));
5381 assert_eq!(
5382 error.value(py).to_string(),
5383 "`order_id_tag` cannot contain the '-' strategy ID separator, was 'A-B'"
5384 );
5385 });
5386 }
5387
5388 #[rstest::rstest]
5389 fn test_py_new_accepts_a_config_without_a_separator() {
5390 pyo3::Python::initialize();
5391 Python::attach(|py| {
5392 let config = StrategyConfig {
5393 order_id_tag: Some("001".to_string()),
5394 ..Default::default()
5395 };
5396 let config_obj = Py::new(py, config).unwrap().into_any();
5397
5398 let strategy = PyStrategy::py_new(Some(config_obj)).unwrap();
5399
5400 assert_eq!(strategy.order_id_tag(), Some("001".to_string()));
5401 });
5402 }
5403
5404 #[rstest::rstest]
5405 fn test_python_aggregate_event_handlers_exist() {
5406 pyo3::Python::initialize();
5407 Python::attach(|py| {
5408 let strategy = Py::new(py, PyStrategy::new(None)).unwrap();
5409 let strategy = strategy.bind(py);
5410
5411 assert!(strategy.hasattr("on_order_event").unwrap());
5412 assert!(strategy.hasattr("on_position_event").unwrap());
5413 });
5414 }
5415
5416 #[rstest::rstest]
5417 #[case(None)]
5418 #[case(Some(25))]
5419 fn test_python_book_depth_subscription_methods_send_commands(#[case] depth: Option<usize>) {
5420 pyo3::Python::initialize();
5421 Python::attach(|py| {
5422 let (_, mut rust_strategy) = create_registered_tracking_strategy(py);
5423 let (handler, saver) = get_typed_into_message_saving_handler::<DataCommand>(None);
5424 msgbus::register_data_command_endpoint(
5425 MessagingSwitchboard::data_engine_queue_execute(),
5426 handler,
5427 );
5428
5429 let instrument_id = sample_instrument().id;
5430 let client_id = Some(ClientId::new("DEPTH-CLIENT"));
5431 rust_strategy
5432 .py_subscribe_book_depth(
5433 &pyo3::Py::new(py, instrument_id)
5434 .unwrap()
5435 .into_bound(py)
5436 .into_any(),
5437 BookType::L2_MBP,
5438 depth,
5439 client_id,
5440 true,
5441 None,
5442 )
5443 .unwrap();
5444 rust_strategy
5445 .py_unsubscribe_book_depth(
5446 &pyo3::Py::new(py, instrument_id)
5447 .unwrap()
5448 .into_bound(py)
5449 .into_any(),
5450 client_id,
5451 None,
5452 )
5453 .unwrap();
5454
5455 let commands = saver.get_messages();
5456 let [
5457 DataCommand::Subscribe(SubscribeCommand::BookDepth(subscribe)),
5458 DataCommand::Unsubscribe(UnsubscribeCommand::BookDepth(unsubscribe)),
5459 ] = commands.as_slice()
5460 else {
5461 panic!("expected BookDepth subscribe and unsubscribe commands, was {commands:?}");
5462 };
5463
5464 assert_eq!(subscribe.single_instrument_id().unwrap(), instrument_id);
5465 assert_eq!(subscribe.book_type, BookType::L2_MBP);
5466 assert_eq!(subscribe.depth.map(NonZeroUsize::get), depth);
5467 assert_eq!(subscribe.client_id, client_id);
5468 assert!(subscribe.managed);
5469 assert_eq!(unsubscribe.single_instrument_id().unwrap(), instrument_id);
5470 assert_eq!(unsubscribe.client_id, client_id);
5471 });
5472 }
5473
5474 #[rstest::rstest]
5475 fn test_strategy_retains_python_config_object() {
5476 pyo3::Python::initialize();
5477 Python::attach(|py| {
5478 let config = py
5479 .eval(
5480 c_str!("type('_Cfg', (), {'strategy_id': 'S-RETAIN-001'})()"),
5481 None,
5482 None,
5483 )
5484 .unwrap();
5485
5486 let strategy = py
5487 .get_type::<PyStrategy>()
5488 .as_any()
5489 .call1((config.clone(),))
5490 .unwrap();
5491
5492 let retained = strategy.getattr("config").unwrap();
5493
5494 assert!(retained.is(&config));
5495 });
5496 }
5497
5498 #[rstest::rstest]
5499 fn test_indicator_registration_exposes_readiness_and_registered_view() {
5500 pyo3::Python::initialize();
5501 Python::attach(|py| {
5502 let mut rust_strategy = PyStrategy::new(None);
5503 let indicator = create_tracking_python_indicator(py).unwrap();
5504 let instrument_id = sample_instrument().id;
5505 let bar_type = sample_bar().bar_type;
5506
5507 assert_eq!(
5508 rust_strategy
5509 .py_registered_indicators(py)
5510 .unwrap()
5511 .bind(py)
5512 .len()
5513 .unwrap(),
5514 0
5515 );
5516 assert!(!rust_strategy.py_indicators_initialized(py).unwrap());
5517
5518 rust_strategy.py_register_indicator_for_quote_ticks(
5519 py,
5520 instrument_id,
5521 indicator.clone_ref(py),
5522 );
5523 rust_strategy.py_register_indicator_for_trade_ticks(
5524 py,
5525 instrument_id,
5526 indicator.clone_ref(py),
5527 );
5528 rust_strategy.py_register_indicator_for_bars(py, bar_type, indicator.clone_ref(py));
5529
5530 let registered = rust_strategy.py_registered_indicators(py).unwrap();
5531 let registered = registered.bind(py);
5532
5533 assert_eq!(registered.len().unwrap(), 1);
5534 assert_eq!(
5535 registered.get_item(0).unwrap().as_ptr(),
5536 indicator.bind(py).as_ptr()
5537 );
5538 assert!(!rust_strategy.py_indicators_initialized(py).unwrap());
5539
5540 indicator.bind(py).setattr("initialized", true).unwrap();
5541
5542 assert!(rust_strategy.py_indicators_initialized(py).unwrap());
5543 });
5544 }
5545
5546 #[rstest::rstest]
5547 fn test_registered_indicators_receive_quote_trade_and_bar_before_strategy_callbacks() {
5548 pyo3::Python::initialize();
5549 Python::attach(|py| {
5550 let events = PyList::empty(py);
5551 let py_strategy = create_indicator_event_strategy(py, &events).unwrap();
5552 let indicator = create_event_tracking_python_indicator(py, &events).unwrap();
5553
5554 let mut rust_strategy = PyStrategy::new(None);
5555 rust_strategy
5556 .set_python_instance(py_strategy.bind(py))
5557 .unwrap();
5558
5559 let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(VirtualClock::new()));
5560 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
5561 let portfolio = Rc::new(RefCell::new(Portfolio::new(
5562 clock.clone(),
5563 cache.clone(),
5564 None,
5565 )));
5566
5567 rust_strategy
5568 .register(TraderId::from("TRADER-001"), clock, cache, portfolio)
5569 .unwrap();
5570 Component::start(rust_strategy.inner_mut()).unwrap();
5571
5572 let quote = sample_quote();
5573 let trade = sample_trade();
5574 let bar = sample_bar();
5575 let external_bar_type = BarType::from_str(&format!(
5576 "{}-1-MINUTE-LAST-EXTERNAL",
5577 bar.bar_type.instrument_id()
5578 ))
5579 .unwrap();
5580
5581 rust_strategy.py_register_indicator_for_quote_ticks(
5582 py,
5583 quote.instrument_id,
5584 indicator.clone_ref(py),
5585 );
5586 rust_strategy.py_register_indicator_for_trade_ticks(
5587 py,
5588 trade.instrument_id,
5589 indicator.clone_ref(py),
5590 );
5591 rust_strategy.py_register_indicator_for_bars(
5592 py,
5593 external_bar_type,
5594 indicator.clone_ref(py),
5595 );
5596
5597 DataActor::handle_quote(rust_strategy.inner_mut(), "e);
5598 DataActor::handle_trade(rust_strategy.inner_mut(), &trade);
5599 DataActor::handle_bar(rust_strategy.inner_mut(), &bar);
5600
5601 let events = events.extract::<Vec<String>>().unwrap();
5602
5603 assert_eq!(python_indicator_call_count(&indicator, py, "quote"), 1);
5604 assert_eq!(python_indicator_call_count(&indicator, py, "trade"), 1);
5605 assert_eq!(python_indicator_call_count(&indicator, py, "bar"), 1);
5606 assert_eq!(
5607 events,
5608 vec![
5609 "indicator:quote",
5610 "strategy:quote",
5611 "indicator:trade",
5612 "strategy:trade",
5613 "indicator:bar",
5614 "strategy:bar",
5615 ]
5616 );
5617 });
5618 }
5619
5620 #[rstest::rstest]
5621 fn test_registered_indicators_receive_historical_quote_trade_and_bar_batches() {
5622 pyo3::Python::initialize();
5623 Python::attach(|py| {
5624 let mut rust_strategy = PyStrategy::new(None);
5625 let indicator = create_tracking_python_indicator(py).unwrap();
5626 let quote = sample_quote();
5627 let trade = sample_trade();
5628 let bar = sample_bar();
5629 let quotes = vec![quote];
5630 let trades = vec![trade];
5631 let bars = vec![bar];
5632
5633 rust_strategy.py_register_indicator_for_quote_ticks(
5634 py,
5635 quote.instrument_id,
5636 indicator.clone_ref(py),
5637 );
5638 rust_strategy.py_register_indicator_for_trade_ticks(
5639 py,
5640 trade.instrument_id,
5641 indicator.clone_ref(py),
5642 );
5643 rust_strategy.py_register_indicator_for_bars(py, bar.bar_type, indicator.clone_ref(py));
5644
5645 let client_id = ClientId::new("TEST");
5646 let quotes_response = QuotesResponse::new(
5647 UUID4::new(),
5648 client_id,
5649 quote.instrument_id,
5650 quotes,
5651 None,
5652 None,
5653 UnixNanos::default(),
5654 None,
5655 );
5656 let trades_response = TradesResponse::new(
5657 UUID4::new(),
5658 client_id,
5659 trade.instrument_id,
5660 trades,
5661 None,
5662 None,
5663 UnixNanos::default(),
5664 None,
5665 );
5666 let bars_response = BarsResponse::new(
5667 UUID4::new(),
5668 client_id,
5669 bar.bar_type,
5670 bars,
5671 None,
5672 None,
5673 UnixNanos::default(),
5674 None,
5675 );
5676
5677 DataActor::handle_quotes_response(rust_strategy.inner_mut(), "es_response);
5678 DataActor::handle_trades_response(rust_strategy.inner_mut(), &trades_response);
5679 DataActor::handle_bars_response(rust_strategy.inner_mut(), &bars_response);
5680
5681 assert_eq!(python_indicator_call_count(&indicator, py, "quote"), 1);
5682 assert_eq!(python_indicator_call_count(&indicator, py, "trade"), 1);
5683 assert_eq!(python_indicator_call_count(&indicator, py, "bar"), 1);
5684 });
5685 }
5686
5687 fn assert_python_dispatch<F>(py: Python<'_>, method_name: &str, invoke: F) -> Py<PyAny>
5688 where
5689 F: FnOnce(&mut PyStrategy) -> anyhow::Result<()>,
5690 {
5691 let (py_strategy, mut rust_strategy) = create_registered_tracking_strategy(py);
5692 let result = invoke(&mut rust_strategy);
5693
5694 assert!(result.is_ok());
5695 assert!(python_method_was_called(&py_strategy, py, method_name));
5696 assert_eq!(python_method_call_count(&py_strategy, py, method_name), 1);
5697
5698 py_strategy
5699 }
5700
5701 fn assert_python_callback<F>(
5702 py: Python<'_>,
5703 method_name: &str,
5704 raises: bool,
5705 shutdown: bool,
5706 invoke: F,
5707 ) -> Py<PyAny>
5708 where
5709 F: Fn(&mut PyStrategy) -> anyhow::Result<()>,
5710 {
5711 let _guard = init_logging(
5712 TraderId::from("TRADER-001"),
5713 UUID4::new(),
5714 Default::default(),
5715 Default::default(),
5716 )
5717 .unwrap();
5718 let (py_strategy, mut rust_strategy) = create_registered_tracking_strategy(py);
5719 if raises {
5720 py_strategy.setattr(py, "raise_on", method_name).unwrap();
5721 }
5722
5723 arm_shutdown_on_error(shutdown);
5724 let result = invoke(&mut rust_strategy);
5725 let trigger = take_shutdown_on_error_trigger();
5726 disarm_shutdown_on_error();
5727
5728 assert!(result.is_ok());
5729 assert_eq!(python_method_call_count(&py_strategy, py, method_name), 1);
5730
5731 if raises && shutdown {
5732 let trigger = trigger.expect("Python callback failure must request shutdown");
5733 assert_eq!(
5734 trigger.component.as_str(),
5735 rust_strategy.inner().core.actor.actor_id.as_str()
5736 );
5737 assert!(
5738 trigger
5739 .message
5740 .contains(&format!("Python {method_name} failed:"))
5741 );
5742 assert!(trigger.message.contains("in fail_callback"));
5743 assert!(trigger.message.contains("ValueError: callback cause"));
5744 assert!(trigger.message.contains("RuntimeError: callback failure"));
5745 } else {
5746 assert_eq!(trigger, None);
5747 }
5748
5749 py_strategy
5750 }
5751
5752 #[rstest::rstest]
5753 #[case(false)]
5754 #[case(true)]
5755 fn test_python_timer_callback_exception_requests_shutdown(#[case] shutdown: bool) {
5756 Python::initialize();
5757 let _guard = init_logging(
5758 TraderId::from("TRADER-001"),
5759 UUID4::new(),
5760 Default::default(),
5761 Default::default(),
5762 )
5763 .unwrap();
5764 Python::attach(|py| {
5765 let (tracker, strategy) = create_registered_tracking_strategy(py);
5766 strategy.register_in_global_registries().unwrap();
5767 Component::start(strategy.inner_mut()).unwrap();
5768 tracker.setattr(py, "raise_on", "on_time_event").unwrap();
5769 let clock = DataActorNative::clock_rc(strategy.inner());
5770 clock
5771 .borrow_mut()
5772 .set_time_alert_ns("CALLBACK", UnixNanos::from(10), None, None)
5773 .unwrap();
5774 arm_shutdown_on_error(shutdown);
5775 let dispatched = dispatch_time_events(&clock, UnixNanos::from(10));
5776 let trigger = take_shutdown_on_error_trigger();
5777 disarm_shutdown_on_error();
5778
5779 assert_eq!(dispatched, 1);
5780 assert_eq!(python_method_call_count(&tracker, py, "on_time_event"), 1);
5781 assert_eq!(Component::state(strategy.inner()), ComponentState::Running);
5782
5783 if shutdown {
5784 let trigger = trigger.expect("strategy timer exception must request shutdown");
5785 assert!(trigger.message.contains("Python on_time_event failed:"));
5786 assert!(trigger.message.contains("in fail_callback"));
5787 assert!(trigger.message.contains("RuntimeError: callback failure"));
5788 } else {
5789 assert_eq!(trigger, None);
5790 }
5791 });
5792 }
5793
5794 #[rstest::rstest]
5795 fn test_python_data_callback_exception_preserves_traceback() {
5796 Python::initialize();
5797 Python::attach(|py| {
5798 let (tracker, strategy) = create_registered_tracking_strategy(py);
5799 tracker.setattr(py, "raise_on", "on_bar").unwrap();
5800 let error = DataActor::on_bar(strategy.inner_mut(), &sample_bar()).unwrap_err();
5801 let message = error.to_string();
5802
5803 assert_eq!(python_method_call_count(&tracker, py, "on_bar"), 1);
5804 assert!(message.contains("Python on_bar failed:"));
5805 assert!(message.contains("in fail_callback"));
5806 assert!(message.contains("ValueError: callback cause"));
5807 assert!(message.contains("RuntimeError: callback failure"));
5808 });
5809 }
5810
5811 #[rstest::rstest]
5812 #[case("on_market_exit")]
5813 #[case("post_market_exit")]
5814 fn test_python_dispatch_market_exit_callback_errors(
5815 #[case] method_name: &str,
5816 #[values(false, true)] raises: bool,
5817 #[values(false, true)] shutdown: bool,
5818 ) {
5819 Python::initialize();
5820 Python::attach(|py| {
5821 assert_python_callback(py, method_name, raises, shutdown, |strategy| {
5822 match method_name {
5823 "on_market_exit" => Strategy::on_market_exit(strategy.inner_mut()),
5824 "post_market_exit" => Strategy::post_market_exit(strategy.inner_mut()),
5825 _ => unreachable!(),
5826 }
5827
5828 Ok(())
5829 });
5830 });
5831 }
5832
5833 #[rstest::rstest]
5834 #[case("on_start")]
5835 #[case("on_stop")]
5836 #[case("on_resume")]
5837 #[case("on_reset")]
5838 #[case("on_dispose")]
5839 #[case("on_degrade")]
5840 #[case("on_fault")]
5841 fn test_python_dispatch_lifecycle_matrix(#[case] method_name: &str) {
5842 pyo3::Python::initialize();
5843 Python::attach(|py| {
5844 assert_python_dispatch(py, method_name, |rust_strategy| match method_name {
5845 "on_start" => DataActor::on_start(rust_strategy.inner_mut()),
5846 "on_stop" => DataActor::on_stop(rust_strategy.inner_mut()),
5847 "on_resume" => DataActor::on_resume(rust_strategy.inner_mut()),
5848 "on_reset" => DataActor::on_reset(rust_strategy.inner_mut()),
5849 "on_dispose" => DataActor::on_dispose(rust_strategy.inner_mut()),
5850 "on_degrade" => DataActor::on_degrade(rust_strategy.inner_mut()),
5851 "on_fault" => DataActor::on_fault(rust_strategy.inner_mut()),
5852 _ => unreachable!("unhandled lifecycle case: {method_name}"),
5853 });
5854 });
5855 }
5856
5857 #[rstest::rstest]
5858 #[case("on_save")]
5859 #[case("on_load")]
5860 fn test_python_dispatch_persistence_matrix(#[case] method_name: &str) {
5861 pyo3::Python::initialize();
5862 Python::attach(|py| {
5863 assert_python_dispatch(py, method_name, |rust_strategy| match method_name {
5864 "on_save" => {
5865 let state = DataActor::on_save(rust_strategy.inner()).unwrap();
5866 assert_eq!(
5867 state.get("strategy").map(Vec::as_slice),
5868 Some(b"saved".as_slice())
5869 );
5870 Ok(())
5871 }
5872 "on_load" => {
5873 let mut state = IndexMap::new();
5874 state.insert("strategy".to_string(), b"loaded".to_vec());
5875 DataActor::on_load(rust_strategy.inner_mut(), state)
5876 }
5877 _ => unreachable!("unhandled persistence case: {method_name}"),
5878 });
5879 });
5880 }
5881
5882 #[rstest::rstest]
5883 fn test_python_persistence_methods_convert_state() {
5884 pyo3::Python::initialize();
5885 Python::attach(|py| {
5886 let (py_strategy, mut rust_strategy) = create_registered_tracking_strategy(py);
5887
5888 let saved = rust_strategy.py_save(py).unwrap();
5889 let saved_state = saved
5890 .bind(py)
5891 .extract::<HashMap<String, Vec<u8>>>()
5892 .unwrap();
5893 assert_eq!(
5894 saved_state.get("strategy").map(Vec::as_slice),
5895 Some(&b"saved"[..])
5896 );
5897
5898 let load_state = PyDict::new(py);
5899 load_state
5900 .set_item("strategy", PyBytes::new(py, b"loaded-from-python"))
5901 .unwrap();
5902
5903 rust_strategy.py_load(&load_state).unwrap();
5904
5905 let loaded_state = python_last_loaded_state(&py_strategy, py).unwrap();
5906 assert_eq!(
5907 loaded_state.get("strategy").map(Vec::as_slice),
5908 Some(&b"loaded-from-python"[..])
5909 );
5910 });
5911 }
5912
5913 #[rstest::rstest]
5914 fn test_python_publish_data_and_signal_reach_msgbus() {
5915 use nautilus_common::msgbus::{
5916 MStr, MessageBus, Pattern, get_message_bus, switchboard::get_custom_topic,
5917 typed_handler::ShareableMessageHandler,
5918 };
5919 use nautilus_core::python::IntoPyObjectNautilusExt;
5920
5921 *get_message_bus().borrow_mut() = MessageBus::default();
5922
5923 pyo3::Python::initialize();
5924 Python::attach(|py| {
5925 let (_, rust_strategy) = create_registered_tracking_strategy(py);
5926 let data = sample_data();
5927
5928 let received_data: Rc<RefCell<Vec<CustomData>>> = Rc::new(RefCell::new(Vec::new()));
5929 let received_data_clone = received_data.clone();
5930 let data_handler = ShareableMessageHandler::from_typed(move |data: &CustomData| {
5931 received_data_clone.borrow_mut().push(data.clone());
5932 });
5933 msgbus::subscribe_any(get_custom_topic(&data.data_type).into(), data_handler, None);
5934
5935 let received_signals: Rc<RefCell<Vec<Signal>>> = Rc::new(RefCell::new(Vec::new()));
5936 let received_signals_clone = received_signals.clone();
5937 let signal_handler = ShareableMessageHandler::from_typed(move |data: &CustomData| {
5938 if let Some(signal) = data.data.as_any().downcast_ref::<Signal>() {
5939 received_signals_clone.borrow_mut().push(signal.clone());
5940 }
5941 });
5942 let signal_pattern: MStr<Pattern> = "data.Signal*".to_string().into();
5943 msgbus::subscribe_any(signal_pattern, signal_handler, None);
5944
5945 rust_strategy
5946 .py_publish_data(&data.data_type, &data)
5947 .unwrap();
5948
5949 let value: Py<PyAny> = 2.0_f64.into_py_any_unwrap(py);
5950 rust_strategy
5951 .py_publish_signal(py, "risk", value, 1_700_000_000_000_000_000)
5952 .unwrap();
5953
5954 let received_data = received_data.borrow();
5955 assert_eq!(received_data.len(), 1);
5956 assert_eq!(received_data[0].data_type, data.data_type);
5957
5958 let received_signals = received_signals.borrow();
5959 assert_eq!(received_signals.len(), 1);
5960 assert_eq!(received_signals[0].name, "risk");
5961 assert_eq!(received_signals[0].value, "2.0");
5962 assert_eq!(
5963 received_signals[0].ts_event,
5964 UnixNanos::from(1_700_000_000_000_000_000_u64),
5965 );
5966 });
5967 }
5968
5969 #[rstest::rstest]
5970 fn test_python_add_and_update_synthetic_update_cache() {
5971 use std::str::FromStr;
5972
5973 use nautilus_model::{
5974 identifiers::{InstrumentId, Symbol},
5975 instruments::SyntheticInstrument,
5976 };
5977
5978 pyo3::Python::initialize();
5979 Python::attach(|py| {
5980 let (_, rust_strategy) = create_registered_tracking_strategy(py);
5981
5982 let comp1 = InstrumentId::from_str("BTC-USD.VENUE").unwrap();
5983 let comp2 = InstrumentId::from_str("ETH-USD.VENUE").unwrap();
5984 let symbol = Symbol::from("SYN");
5985 let original_formula = format!("({comp1} + {comp2}) / 2.0");
5986 let synthetic = SyntheticInstrument::builder()
5987 .symbol(symbol)
5988 .price_precision(2)
5989 .components(vec![comp1, comp2])
5990 .formula(&original_formula)
5991 .ts_event(UnixNanos::default())
5992 .ts_init(UnixNanos::default())
5993 .build()
5994 .unwrap();
5995 let synthetic_id = synthetic.id;
5996
5997 rust_strategy.py_add_synthetic(synthetic).unwrap();
5998
5999 let updated_formula = format!("{comp1} + {comp2}");
6000 let updated = SyntheticInstrument::builder()
6001 .symbol(symbol)
6002 .price_precision(2)
6003 .components(vec![comp1, comp2])
6004 .formula(&updated_formula)
6005 .ts_event(UnixNanos::default())
6006 .ts_init(UnixNanos::default())
6007 .build()
6008 .unwrap();
6009 rust_strategy.py_update_synthetic(updated).unwrap();
6010
6011 let cache = DataActor::cache(rust_strategy.inner());
6012 let stored = cache.synthetic(&synthetic_id).unwrap();
6013 assert_eq!(stored.formula, updated_formula);
6014 });
6015 }
6016
6017 #[rstest::rstest]
6018 fn test_python_subscribe_and_unsubscribe_signal_update_msgbus() {
6019 use nautilus_common::msgbus::{MessageBus, get_message_bus, switchboard::get_signal_topic};
6020
6021 *get_message_bus().borrow_mut() = MessageBus::default();
6022
6023 pyo3::Python::initialize();
6024 Python::attach(|py| {
6025 let (_, mut rust_strategy) = create_registered_tracking_strategy(py);
6026
6027 rust_strategy.py_subscribe_signal("risk", Some(50)).unwrap();
6028
6029 let topic = get_signal_topic("risk");
6030 let subscriptions = get_message_bus().borrow_mut().matching_subscriptions(topic);
6031 assert_eq!(subscriptions.len(), 1);
6032 assert_eq!(subscriptions[0].priority, 50);
6033
6034 rust_strategy.py_unsubscribe_signal("risk").unwrap();
6035
6036 let subscriptions = get_message_bus().borrow_mut().matching_subscriptions(topic);
6037 assert!(subscriptions.is_empty());
6038 });
6039 }
6040
6041 #[rstest::rstest]
6042 #[case(None)]
6043 #[case(Some(SystemChannel::ExecCommands))]
6044 fn test_python_subscribe_and_unsubscribe_queue_state_update_msgbus(
6045 #[case] channel: Option<SystemChannel>,
6046 ) {
6047 use nautilus_common::msgbus::{MessageBus, MessagingSwitchboard, get_message_bus};
6048
6049 *get_message_bus().borrow_mut() = MessageBus::default();
6050
6051 pyo3::Python::initialize();
6052 Python::attach(|py| {
6053 let (_, mut rust_strategy) = create_registered_tracking_strategy(py);
6054
6055 rust_strategy
6056 .py_subscribe_queue_state(channel, Some(50))
6057 .unwrap();
6058
6059 let topic =
6060 MessagingSwitchboard::queue_state_changed_topic(SystemChannel::ExecCommands);
6061 let subscriptions = get_message_bus().borrow_mut().matching_subscriptions(topic);
6062 assert_eq!(subscriptions.len(), 1);
6063 assert_eq!(subscriptions[0].priority, 50);
6064 let unrelated =
6065 MessagingSwitchboard::queue_state_changed_topic(SystemChannel::DataEvents);
6066 assert_eq!(
6067 get_message_bus()
6068 .borrow_mut()
6069 .matching_subscriptions(unrelated)
6070 .len(),
6071 usize::from(channel.is_none())
6072 );
6073
6074 rust_strategy.py_unsubscribe_queue_state(channel).unwrap();
6075
6076 let subscriptions = get_message_bus().borrow_mut().matching_subscriptions(topic);
6077 assert!(subscriptions.is_empty());
6078 });
6079 }
6080
6081 #[rstest::rstest]
6082 #[case(None, None)]
6083 #[case(Some(ClientId::from("BINANCE")), None)]
6084 #[case(None, Some("binance-futures-market-streams"))]
6085 #[case(
6086 Some(ClientId::from("BINANCE")),
6087 Some("binance-futures-market-streams")
6088 )]
6089 fn test_python_subscribe_and_unsubscribe_socket_state_update_msgbus(
6090 #[case] client_id: Option<ClientId>,
6091 #[case] endpoint: Option<&str>,
6092 ) {
6093 use nautilus_common::msgbus::{MessageBus, MessagingSwitchboard, get_message_bus};
6094
6095 *get_message_bus().borrow_mut() = MessageBus::default();
6096
6097 pyo3::Python::initialize();
6098 Python::attach(|py| {
6099 let (_, mut rust_strategy) = create_registered_tracking_strategy(py);
6100
6101 rust_strategy
6102 .py_subscribe_socket_state(client_id, endpoint, Some(50))
6103 .unwrap();
6104
6105 let topic = MessagingSwitchboard::socket_state_changed_topic(
6106 ClientId::from("BINANCE"),
6107 "binance-futures-market-streams",
6108 );
6109 let subscriptions = get_message_bus().borrow_mut().matching_subscriptions(topic);
6110 assert_eq!(subscriptions.len(), 1);
6111 assert_eq!(subscriptions[0].priority, 50);
6112 let unrelated =
6113 MessagingSwitchboard::socket_state_changed_topic(ClientId::from("BYBIT"), "orders");
6114 assert_eq!(
6115 get_message_bus()
6116 .borrow_mut()
6117 .matching_subscriptions(unrelated)
6118 .len(),
6119 usize::from(client_id.is_none() && endpoint.is_none())
6120 );
6121
6122 rust_strategy
6123 .py_unsubscribe_socket_state(client_id, endpoint)
6124 .unwrap();
6125
6126 let subscriptions = get_message_bus().borrow_mut().matching_subscriptions(topic);
6127 assert!(subscriptions.is_empty());
6128 });
6129 }
6130
6131 #[rstest::rstest]
6132 fn test_python_reconnect_socket_enqueues_typed_command() {
6133 pyo3::Python::initialize();
6134 Python::attach(|py| {
6135 let (system_tx, mut system_rx) = tokio::sync::mpsc::unbounded_channel();
6136 replace_system_command_sender(system_tx);
6137 let (_, strategy) = create_registered_tracking_strategy(py);
6138
6139 strategy
6140 .py_reconnect_socket(ClientId::from("POLYMARKET"), "polymarket-market-streams")
6141 .expect("valid reconnect command");
6142 let command = system_rx
6143 .try_recv()
6144 .expect("reconnect command should be queued");
6145 let SystemCommand::ReconnectSocket(command) = command;
6146
6147 assert_eq!(command.trader_id, TraderId::from("TRADER-001"));
6148 assert_eq!(command.client_id, ClientId::from("POLYMARKET"));
6149 assert_eq!(command.endpoint, "polymarket-market-streams");
6150 assert_eq!(command.ts_init, UnixNanos::default());
6151 });
6152 }
6153
6154 #[rstest::rstest]
6155 fn test_python_stop_stops_immediately_when_manage_stop_disabled() {
6156 pyo3::Python::initialize();
6157 Python::attach(|py| {
6158 let config = StrategyConfig {
6159 strategy_id: Some(StrategyId::from("TEST-001")),
6160 order_id_tag: Some("001".to_string()),
6161 manage_stop: false,
6162 ..Default::default()
6163 };
6164 let (py_strategy, mut rust_strategy) =
6165 create_registered_tracking_strategy_with_config(py, Some(config));
6166
6167 rust_strategy.py_start().unwrap();
6168 rust_strategy.py_stop().unwrap();
6169
6170 assert!(rust_strategy.py_is_stopped());
6171 assert!(!rust_strategy.inner().core.pending_stop);
6172 assert!(!rust_strategy.inner().core.is_exiting);
6173 assert_eq!(python_method_call_count(&py_strategy, py, "on_stop"), 1);
6174 });
6175 }
6176
6177 #[rstest::rstest]
6178 fn test_python_stop_defers_when_manage_stop_enabled() {
6179 pyo3::Python::initialize();
6180 Python::attach(|py| {
6181 let config = StrategyConfig {
6182 strategy_id: Some(StrategyId::from("TEST-001")),
6183 order_id_tag: Some("001".to_string()),
6184 manage_stop: true,
6185 ..Default::default()
6186 };
6187 let (py_strategy, mut rust_strategy) =
6188 create_registered_tracking_strategy_with_config(py, Some(config));
6189
6190 rust_strategy.py_start().unwrap();
6191 rust_strategy.py_stop().unwrap();
6192
6193 assert!(rust_strategy.py_is_running());
6194 assert!(rust_strategy.inner().core.pending_stop);
6195 assert!(rust_strategy.inner().core.is_exiting);
6196 assert_eq!(python_method_call_count(&py_strategy, py, "on_stop"), 0);
6197 });
6198 }
6199
6200 #[rstest::rstest]
6201 fn test_python_market_exit_methods_update_state_and_dispatch_hooks() {
6202 pyo3::Python::initialize();
6203 Python::attach(|py| {
6204 let (py_strategy, mut rust_strategy) = create_registered_tracking_strategy(py);
6205
6206 rust_strategy.py_start().unwrap();
6207
6208 assert!(!rust_strategy.py_is_exiting());
6209
6210 rust_strategy.py_market_exit().unwrap();
6211
6212 assert!(rust_strategy.py_is_exiting());
6213 assert_eq!(
6214 python_method_call_count(&py_strategy, py, "on_market_exit"),
6215 1
6216 );
6217
6218 rust_strategy.inner_mut().finalize_market_exit();
6219
6220 assert!(!rust_strategy.py_is_exiting());
6221 assert_eq!(
6222 python_method_call_count(&py_strategy, py, "post_market_exit"),
6223 1
6224 );
6225 });
6226 }
6227
6228 #[rstest::rstest]
6229 #[case::order_list_object(true)]
6230 #[case::raw_order_sequence(false)]
6231 fn test_python_submit_order_list_accepts_order_list_inputs(#[case] wrap_order_list: bool) {
6232 pyo3::Python::initialize();
6233 Python::attach(|py| {
6234 let (_, mut rust_strategy) = create_registered_tracking_strategy(py);
6235 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6236 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
6237 msgbus::register_trading_command_endpoint(
6238 MessagingSwitchboard::risk_engine_queue_execute(),
6239 risk_handler,
6240 );
6241
6242 let strategy_id = rust_strategy.strategy_id();
6243 let client_order_id1 = ClientOrderId::from("O-PYO3-LIST-001");
6244 let client_order_id2 = ClientOrderId::from("O-PYO3-LIST-002");
6245 let orders = vec![
6246 sample_python_market_order(py, strategy_id, client_order_id1).unwrap(),
6247 sample_python_market_order(py, strategy_id, client_order_id2).unwrap(),
6248 ];
6249 let params = PyDict::new(py);
6250
6251 params.set_item("routing_hint", "prefer_batch").unwrap();
6252 let order_list = if wrap_order_list {
6253 let order_list_type = py
6254 .eval(c_str!("type('OrderListShim', (), {})"), None, None)
6255 .unwrap();
6256 let order_list = order_list_type.call0().unwrap();
6257
6258 order_list.setattr("orders", orders).unwrap();
6259 order_list.unbind()
6260 } else {
6261 PyList::new(py, orders).unwrap().into_any().unbind()
6262 };
6263
6264 rust_strategy
6265 .py_submit_order_list(py, order_list, None, None, Some(params.unbind()))
6266 .unwrap();
6267
6268 let cache = DataActor::cache(rust_strategy.inner());
6269 let cached_order1 = cache.order(&client_order_id1).unwrap();
6270 let cached_order2 = cache.order(&client_order_id2).unwrap();
6271 let order_list_id = cached_order1.order_list_id().unwrap();
6272 let order_list = cache.order_list(&order_list_id).unwrap();
6273
6274 assert_eq!(cached_order2.order_list_id(), Some(order_list_id));
6275 assert_eq!(
6276 order_list.client_order_ids.as_slice(),
6277 &[client_order_id1, client_order_id2]
6278 );
6279
6280 let py_cache =
6281 Py::new(py, PyCache::from_rc(rust_strategy.inner().core.cache_rc())).unwrap();
6282 let py_order_list = py_cache
6283 .bind(py)
6284 .call_method1("order_list", (order_list_id,))
6285 .unwrap();
6286
6287 assert_eq!(
6288 py_order_list
6289 .getattr("id")
6290 .unwrap()
6291 .extract::<OrderListId>()
6292 .unwrap(),
6293 order_list_id,
6294 );
6295 assert_eq!(
6296 py_order_list
6297 .call_method0("client_order_ids")
6298 .unwrap()
6299 .extract::<Vec<ClientOrderId>>()
6300 .unwrap(),
6301 vec![client_order_id1, client_order_id2],
6302 );
6303
6304 let risk_messages = risk_messages.get_messages();
6305 assert_eq!(risk_messages.len(), 1);
6306 let Some(TradingCommand::SubmitOrderList(command)) = risk_messages.first() else {
6307 panic!("expected SubmitOrderList command");
6308 };
6309 assert_eq!(
6310 command
6311 .params
6312 .as_ref()
6313 .and_then(|params| params.get("routing_hint")),
6314 Some(&Value::String("prefer_batch".to_string()))
6315 );
6316 });
6317 }
6318
6319 #[rstest::rstest]
6320 fn test_python_close_position_forwards_params_to_submit_order() {
6321 pyo3::Python::initialize();
6322 Python::attach(|py| {
6323 let (_, mut rust_strategy) = create_registered_tracking_strategy(py);
6324 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6325 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
6326 msgbus::register_trading_command_endpoint(
6327 MessagingSwitchboard::risk_engine_queue_execute(),
6328 risk_handler,
6329 );
6330
6331 let position_id = PositionId::from("P-PYO3-CLOSE-001");
6332 let position = sample_open_position(
6333 rust_strategy.strategy_id(),
6334 position_id,
6335 ClientOrderId::from("O-PYO3-CLOSE-001"),
6336 );
6337 rust_strategy
6338 .inner()
6339 .core
6340 .cache_rc()
6341 .borrow_mut()
6342 .add_instrument(InstrumentAny::CurrencyPair(sample_instrument()))
6343 .unwrap();
6344 let params = PyDict::new(py);
6345
6346 params.set_item("routing_hint", "close_single").unwrap();
6347 rust_strategy
6348 .py_close_position(
6349 &position,
6350 None,
6351 None,
6352 None,
6353 None,
6354 None,
6355 Some(params.unbind()),
6356 )
6357 .unwrap();
6358
6359 let risk_messages = risk_messages.get_messages();
6360 assert_eq!(risk_messages.len(), 1);
6361 let Some(TradingCommand::SubmitOrder(command)) = risk_messages.first() else {
6362 panic!("expected SubmitOrder command");
6363 };
6364 assert_eq!(command.position_id, Some(position_id));
6365 assert_eq!(
6366 command
6367 .params
6368 .as_ref()
6369 .and_then(|params| params.get("routing_hint")),
6370 Some(&Value::String("close_single".to_string()))
6371 );
6372 });
6373 }
6374
6375 #[rstest::rstest]
6376 fn test_python_close_all_positions_forwards_params_to_submit_order() {
6377 pyo3::Python::initialize();
6378 Python::attach(|py| {
6379 let (_, mut rust_strategy) = create_registered_tracking_strategy(py);
6380 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6381 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
6382 msgbus::register_trading_command_endpoint(
6383 MessagingSwitchboard::risk_engine_queue_execute(),
6384 risk_handler,
6385 );
6386
6387 let instrument = sample_instrument();
6388 let position_id1 = PositionId::from("P-PYO3-CLOSE-ALL-001");
6389 let position_id2 = PositionId::from("P-PYO3-CLOSE-ALL-002");
6390 let position1 = sample_open_position(
6391 rust_strategy.strategy_id(),
6392 position_id1,
6393 ClientOrderId::from("O-PYO3-CLOSE-ALL-001"),
6394 );
6395 let position2 = sample_open_position(
6396 rust_strategy.strategy_id(),
6397 position_id2,
6398 ClientOrderId::from("O-PYO3-CLOSE-ALL-002"),
6399 );
6400 let cache = rust_strategy.inner().core.cache_rc();
6401
6402 {
6403 let mut cache = cache.borrow_mut();
6404 cache
6405 .add_instrument(InstrumentAny::CurrencyPair(instrument.clone()))
6406 .unwrap();
6407 cache.add_position(&position1, OmsType::Hedging).unwrap();
6408 cache.add_position(&position2, OmsType::Hedging).unwrap();
6409 }
6410
6411 let params = PyDict::new(py);
6412
6413 params.set_item("routing_hint", "close_all").unwrap();
6414 rust_strategy
6415 .py_close_all_positions(
6416 instrument.id,
6417 None,
6418 None,
6419 None,
6420 None,
6421 None,
6422 None,
6423 Some(params.unbind()),
6424 )
6425 .unwrap();
6426
6427 let risk_messages = risk_messages.get_messages();
6428 assert_eq!(risk_messages.len(), 2);
6429 let commands: Vec<_> = risk_messages
6430 .iter()
6431 .map(|message| {
6432 let TradingCommand::SubmitOrder(command) = message else {
6433 panic!("expected SubmitOrder command");
6434 };
6435 command
6436 })
6437 .collect();
6438
6439 assert!(
6440 commands
6441 .iter()
6442 .any(|command| command.position_id == Some(position_id1))
6443 );
6444 assert!(
6445 commands
6446 .iter()
6447 .any(|command| command.position_id == Some(position_id2))
6448 );
6449 assert!(commands.iter().all(|command| {
6450 command
6451 .params
6452 .as_ref()
6453 .and_then(|params| params.get("routing_hint"))
6454 == Some(&Value::String("close_all".to_string()))
6455 }));
6456 });
6457 }
6458
6459 #[rstest::rstest]
6460 fn test_python_cancel_gtd_expiry_accepts_order() {
6461 pyo3::Python::initialize();
6462 Python::attach(|py| {
6463 let (_, mut rust_strategy) = create_registered_tracking_strategy(py);
6464 let strategy_id = rust_strategy.strategy_id();
6465 let client_order_id = ClientOrderId::from("O-PYO3-GTD-001");
6466 let timer_name = format!("GTD-EXPIRY:{client_order_id}");
6467 let order = OrderTestBuilder::new(OrderType::Limit)
6468 .trader_id(TraderId::from("TRADER-001"))
6469 .strategy_id(strategy_id)
6470 .instrument_id(sample_instrument().id)
6471 .client_order_id(client_order_id)
6472 .quantity(Quantity::from(100_000))
6473 .price(Price::from("1.00000"))
6474 .time_in_force(TimeInForce::Gtd)
6475 .expire_time(UnixNanos::from(1))
6476 .build();
6477 let py_order = order_any_to_pyobject(py, order).unwrap();
6478
6479 {
6480 let mut clock = rust_strategy.inner_mut().core.clock_mut();
6481 clock
6482 .set_time_alert_ns(&timer_name, UnixNanos::from(1), None, None)
6483 .unwrap();
6484 }
6485 rust_strategy
6486 .inner_mut()
6487 .core
6488 .gtd_timers
6489 .insert(client_order_id, Ustr::from(&timer_name));
6490
6491 rust_strategy
6492 .py_cancel_gtd_expiry(py, py_order)
6493 .expect("cancel_gtd_expiry should accept Python order");
6494
6495 let clock_timer_exists = rust_strategy
6496 .inner_mut()
6497 .core
6498 .clock_mut()
6499 .timer_names()
6500 .contains(&timer_name.as_str());
6501
6502 assert!(
6503 !rust_strategy
6504 .inner_mut()
6505 .has_gtd_expiry_timer(&client_order_id)
6506 );
6507 assert!(!clock_timer_exists);
6508 });
6509 }
6510
6511 #[rstest::rstest]
6512 #[case("on_time_event")]
6513 #[case("on_data")]
6514 #[case("on_signal")]
6515 #[case("on_queue_state")]
6516 #[case("on_socket_state")]
6517 #[case("on_instrument")]
6518 #[case("on_quote")]
6519 #[case("on_trade")]
6520 #[case("on_bar")]
6521 #[case("on_book_deltas")]
6522 #[case("on_book_depth")]
6523 #[case("on_book")]
6524 #[case("on_mark_price")]
6525 #[case("on_index_price")]
6526 #[case("on_funding_rate")]
6527 #[case("on_instrument_status")]
6528 #[case("on_instrument_close")]
6529 #[case("on_option_greeks")]
6530 #[case("on_option_chain")]
6531 #[case("on_historical_data")]
6532 #[case("on_historical_book_deltas")]
6533 #[case("on_historical_book_depth")]
6534 #[case("on_historical_quotes")]
6535 #[case("on_historical_trades")]
6536 #[case("on_historical_funding_rates")]
6537 #[case("on_historical_bars")]
6538 #[case("on_historical_mark_prices")]
6539 #[case("on_historical_index_prices")]
6540 fn test_python_dispatch_data_callback_matrix(#[case] method_name: &str) {
6541 pyo3::Python::initialize();
6542 Python::attach(|py| {
6543 assert_python_dispatch(py, method_name, |rust_strategy| match method_name {
6544 "on_time_event" => {
6545 let event = sample_time_event();
6546 DataActor::on_time_event(rust_strategy.inner_mut(), &event)
6547 }
6548 "on_data" => {
6549 let data = sample_data();
6550 rust_strategy.inner_mut().on_data(&data)
6551 }
6552 "on_signal" => {
6553 let signal = sample_signal();
6554 rust_strategy.inner_mut().on_signal(&signal)
6555 }
6556 "on_queue_state" => {
6557 let event = sample_queue_state_changed();
6558 rust_strategy.inner_mut().on_queue_state(&event)
6559 }
6560 "on_socket_state" => {
6561 let event = sample_socket_state_changed();
6562 rust_strategy.inner_mut().on_socket_state(&event)
6563 }
6564 "on_instrument" => {
6565 let instrument = InstrumentAny::CurrencyPair(sample_instrument());
6566 rust_strategy.inner_mut().on_instrument(&instrument)
6567 }
6568 "on_quote" => {
6569 let quote = sample_quote();
6570 rust_strategy.inner_mut().on_quote("e)
6571 }
6572 "on_trade" => {
6573 let trade = sample_trade();
6574 rust_strategy.inner_mut().on_trade(&trade)
6575 }
6576 "on_bar" => {
6577 let bar = sample_bar();
6578 rust_strategy.inner_mut().on_bar(&bar)
6579 }
6580 "on_book_deltas" => {
6581 let deltas = sample_book_deltas();
6582 rust_strategy.inner_mut().on_book_deltas(&deltas)
6583 }
6584 "on_book_depth" => {
6585 let depth = sample_book_depth();
6586 rust_strategy.inner_mut().on_book_depth(&depth)
6587 }
6588 "on_book" => {
6589 let book = sample_book();
6590 rust_strategy.inner_mut().on_book(&book)
6591 }
6592 "on_mark_price" => {
6593 let mark_price = sample_mark_price();
6594 rust_strategy.inner_mut().on_mark_price(&mark_price)
6595 }
6596 "on_index_price" => {
6597 let index_price = sample_index_price();
6598 rust_strategy.inner_mut().on_index_price(&index_price)
6599 }
6600 "on_funding_rate" => {
6601 let funding_rate = sample_funding_rate();
6602 rust_strategy.inner_mut().on_funding_rate(&funding_rate)
6603 }
6604 "on_instrument_status" => {
6605 let status = sample_instrument_status();
6606 rust_strategy.inner_mut().on_instrument_status(&status)
6607 }
6608 "on_instrument_close" => {
6609 let close = sample_instrument_close();
6610 rust_strategy.inner_mut().on_instrument_close(&close)
6611 }
6612 "on_option_greeks" => {
6613 let greeks = sample_option_greeks();
6614 DataActor::on_option_greeks(rust_strategy.inner_mut(), &greeks)
6615 }
6616 "on_option_chain" => {
6617 let slice = sample_option_chain();
6618 DataActor::on_option_chain(rust_strategy.inner_mut(), &slice)
6619 }
6620 "on_historical_data" => {
6621 let data = sample_data();
6622 rust_strategy.inner_mut().on_historical_data(&data)
6623 }
6624 "on_historical_book_deltas" => {
6625 let deltas = sample_book_deltas().deltas;
6626 rust_strategy.inner_mut().on_historical_book_deltas(&deltas)
6627 }
6628 "on_historical_book_depth" => {
6629 let depths = vec![sample_book_depth()];
6630 rust_strategy.inner_mut().on_historical_book_depth(&depths)
6631 }
6632 "on_historical_quotes" => {
6633 let quotes = vec![sample_quote()];
6634 rust_strategy.inner_mut().on_historical_quotes("es)
6635 }
6636 "on_historical_trades" => {
6637 let trades = vec![sample_trade()];
6638 rust_strategy.inner_mut().on_historical_trades(&trades)
6639 }
6640 "on_historical_funding_rates" => {
6641 let funding_rates = vec![sample_funding_rate()];
6642 rust_strategy
6643 .inner_mut()
6644 .on_historical_funding_rates(&funding_rates)
6645 }
6646 "on_historical_bars" => {
6647 let bars = vec![sample_bar()];
6648 rust_strategy.inner_mut().on_historical_bars(&bars)
6649 }
6650 "on_historical_mark_prices" => {
6651 let mark_prices = vec![sample_mark_price()];
6652 rust_strategy
6653 .inner_mut()
6654 .on_historical_mark_prices(&mark_prices)
6655 }
6656 "on_historical_index_prices" => {
6657 let index_prices = vec![sample_index_price()];
6658 rust_strategy
6659 .inner_mut()
6660 .on_historical_index_prices(&index_prices)
6661 }
6662 _ => unreachable!("unhandled data callback case: {method_name}"),
6663 });
6664 });
6665 }
6666
6667 #[rstest::rstest]
6668 fn test_python_dispatch_historical_custom_data_preserves_payload_shape() {
6669 pyo3::Python::initialize();
6670
6671 Python::attach(|py| {
6672 let scalar = stub_custom_data(3, 126, None, None);
6673 let py_strategy = assert_python_dispatch(py, "on_historical_data", |rust_strategy| {
6674 rust_strategy.inner_mut().on_historical_data(&scalar)
6675 });
6676 let actual_scalar = py_strategy
6677 .call_method1(py, "last_call_args", ("on_historical_data",))
6678 .unwrap()
6679 .bind(py)
6680 .get_item(0)
6681 .unwrap()
6682 .extract::<CustomData>()
6683 .unwrap();
6684
6685 assert_eq!(actual_scalar, scalar);
6686
6687 let expected = vec![
6688 stub_custom_data(1, 42, None, None),
6689 stub_custom_data(2, 84, None, None),
6690 ];
6691 let py_strategy = assert_python_dispatch(py, "on_historical_data", |rust_strategy| {
6692 rust_strategy.inner_mut().on_historical_data(&expected)
6693 });
6694 let actual = py_strategy
6695 .call_method1(py, "last_call_args", ("on_historical_data",))
6696 .unwrap()
6697 .bind(py)
6698 .get_item(0)
6699 .unwrap()
6700 .extract::<Vec<CustomData>>()
6701 .unwrap();
6702
6703 assert_eq!(actual, expected);
6704 });
6705 }
6706
6707 #[rstest::rstest]
6708 fn test_python_dispatch_historical_book_deltas_preserves_batch() {
6709 pyo3::Python::initialize();
6710
6711 Python::attach(|py| {
6712 let expected = stub_deltas().deltas;
6713 let py_strategy =
6714 assert_python_dispatch(py, "on_historical_book_deltas", |rust_strategy| {
6715 rust_strategy
6716 .inner_mut()
6717 .on_historical_book_deltas(&expected)
6718 });
6719 let actual = py_strategy
6720 .call_method1(py, "last_call_args", ("on_historical_book_deltas",))
6721 .unwrap()
6722 .bind(py)
6723 .get_item(0)
6724 .unwrap()
6725 .extract::<Vec<OrderBookDelta>>()
6726 .unwrap();
6727
6728 assert_eq!(actual, expected);
6729 });
6730 }
6731
6732 #[rstest::rstest]
6733 fn test_python_dispatch_historical_book_depth_preserves_batch() {
6734 pyo3::Python::initialize();
6735
6736 Python::attach(|py| {
6737 let first = stub_depth10();
6738 let mut second = first.clone();
6739 second.sequence = 17;
6740 second.ts_event = UnixNanos::from(18);
6741 second.ts_init = UnixNanos::from(19);
6742 let expected = vec![first, second];
6743 let py_strategy =
6744 assert_python_dispatch(py, "on_historical_book_depth", |rust_strategy| {
6745 rust_strategy
6746 .inner_mut()
6747 .on_historical_book_depth(&expected)
6748 });
6749 let actual = py_strategy
6750 .call_method1(py, "last_call_args", ("on_historical_book_depth",))
6751 .unwrap()
6752 .bind(py)
6753 .get_item(0)
6754 .unwrap()
6755 .extract::<Vec<OrderBookDepth>>()
6756 .unwrap();
6757
6758 assert_eq!(actual, expected);
6759 });
6760 }
6761
6762 #[rstest::rstest]
6763 #[case("on_order_initialized")]
6764 #[case("on_order_event")]
6765 #[case("on_order_denied")]
6766 #[case("on_order_emulated")]
6767 #[case("on_order_released")]
6768 #[case("on_order_submitted")]
6769 #[case("on_order_rejected")]
6770 #[case("on_order_accepted")]
6771 #[case("on_order_expired")]
6772 #[case("on_order_triggered")]
6773 #[case("on_order_pending_update")]
6774 #[case("on_order_pending_cancel")]
6775 #[case("on_order_modify_rejected")]
6776 #[case("on_order_cancel_rejected")]
6777 #[case("on_order_updated")]
6778 #[case("on_order_canceled")]
6779 #[case("on_order_filled")]
6780 #[case("on_order_fill_voided")]
6781 fn test_python_dispatch_order_callback_matrix(
6782 #[case] method_name: &str,
6783 #[values(false, true)] raises: bool,
6784 #[values(false, true)] shutdown: bool,
6785 ) {
6786 pyo3::Python::initialize();
6787 Python::attach(|py| {
6788 assert_python_callback(py, method_name, raises, shutdown, |rust_strategy| {
6789 match method_name {
6790 "on_order_initialized" => {
6791 Strategy::on_order_initialized(
6792 rust_strategy.inner_mut(),
6793 OrderInitialized::default(),
6794 );
6795 Ok(())
6796 }
6797 "on_order_event" => {
6798 Strategy::on_order_event(
6799 rust_strategy.inner_mut(),
6800 OrderEventAny::Accepted(OrderAccepted::default()),
6801 );
6802 Ok(())
6803 }
6804 "on_order_denied" => {
6805 Strategy::on_order_denied(
6806 rust_strategy.inner_mut(),
6807 OrderDenied::default(),
6808 );
6809 Ok(())
6810 }
6811 "on_order_emulated" => {
6812 Strategy::on_order_emulated(
6813 rust_strategy.inner_mut(),
6814 OrderEmulated::default(),
6815 );
6816 Ok(())
6817 }
6818 "on_order_released" => {
6819 Strategy::on_order_released(
6820 rust_strategy.inner_mut(),
6821 OrderReleased::default(),
6822 );
6823 Ok(())
6824 }
6825 "on_order_submitted" => {
6826 Strategy::on_order_submitted(
6827 rust_strategy.inner_mut(),
6828 OrderSubmitted::default(),
6829 );
6830 Ok(())
6831 }
6832 "on_order_rejected" => {
6833 Strategy::on_order_rejected(
6834 rust_strategy.inner_mut(),
6835 OrderRejected::default(),
6836 );
6837 Ok(())
6838 }
6839 "on_order_accepted" => {
6840 Strategy::on_order_accepted(
6841 rust_strategy.inner_mut(),
6842 OrderAccepted::default(),
6843 );
6844 Ok(())
6845 }
6846 "on_order_expired" => {
6847 Strategy::on_order_expired(
6848 rust_strategy.inner_mut(),
6849 OrderExpired::default(),
6850 );
6851 Ok(())
6852 }
6853 "on_order_triggered" => {
6854 Strategy::on_order_triggered(
6855 rust_strategy.inner_mut(),
6856 OrderTriggered::default(),
6857 );
6858 Ok(())
6859 }
6860 "on_order_pending_update" => {
6861 Strategy::on_order_pending_update(
6862 rust_strategy.inner_mut(),
6863 OrderPendingUpdate::default(),
6864 );
6865 Ok(())
6866 }
6867 "on_order_pending_cancel" => {
6868 Strategy::on_order_pending_cancel(
6869 rust_strategy.inner_mut(),
6870 OrderPendingCancel::default(),
6871 );
6872 Ok(())
6873 }
6874 "on_order_modify_rejected" => {
6875 Strategy::on_order_modify_rejected(
6876 rust_strategy.inner_mut(),
6877 OrderModifyRejected::default(),
6878 );
6879 Ok(())
6880 }
6881 "on_order_cancel_rejected" => {
6882 Strategy::on_order_cancel_rejected(
6883 rust_strategy.inner_mut(),
6884 OrderCancelRejected::default(),
6885 );
6886 Ok(())
6887 }
6888 "on_order_updated" => {
6889 Strategy::on_order_updated(
6890 rust_strategy.inner_mut(),
6891 OrderUpdated::default(),
6892 );
6893 Ok(())
6894 }
6895 "on_order_canceled" => {
6896 let event = OrderCanceled::default();
6897 Strategy::on_order_canceled(rust_strategy.inner_mut(), &event);
6898 Ok(())
6899 }
6900 "on_order_filled" => {
6901 let event = OrderFilledSpec::builder().build();
6902 Strategy::on_order_filled(rust_strategy.inner_mut(), &event);
6903 Ok(())
6904 }
6905 "on_order_fill_voided" => {
6906 let event = OrderFillVoidedSpec::builder().build();
6907 Strategy::on_order_fill_voided(rust_strategy.inner_mut(), &event);
6908 Ok(())
6909 }
6910 _ => unreachable!("unhandled order callback case: {method_name}"),
6911 }
6912 });
6913 });
6914 }
6915
6916 #[rstest::rstest]
6917 #[case("on_order_filled")]
6918 #[case("on_order_event")]
6919 fn test_python_order_event_callback_exception_continues_dispatch(
6920 #[case] method_name: &str,
6921 #[values(false, true)] shutdown: bool,
6922 ) {
6923 Python::initialize();
6924 Python::attach(|py| {
6925 let tracker = assert_python_callback(py, method_name, true, shutdown, |strategy| {
6926 Component::start(strategy.inner_mut()).unwrap();
6927 Strategy::handle_order_event(
6928 strategy.inner_mut(),
6929 OrderEventAny::Filled(OrderFilledSpec::builder().build()),
6930 );
6931 assert_eq!(Component::state(strategy.inner()), ComponentState::Running);
6932 Ok(())
6933 });
6934
6935 assert_eq!(
6936 python_method_call_names(&tracker, py),
6937 ["on_start", "on_order_filled", "on_order_event"],
6938 );
6939 });
6940 }
6941
6942 #[rstest::rstest]
6943 fn test_python_handle_order_event_dispatches_specific_and_aggregate_callbacks() {
6944 pyo3::Python::initialize();
6945 Python::attach(|py| {
6946 let (py_strategy, mut rust_strategy) = create_registered_tracking_strategy(py);
6947
6948 rust_strategy.py_start().unwrap();
6949 Strategy::handle_order_event(
6950 rust_strategy.inner_mut(),
6951 OrderEventAny::Accepted(OrderAccepted::default()),
6952 );
6953
6954 assert_eq!(
6955 python_method_call_count(&py_strategy, py, "on_order_accepted"),
6956 1
6957 );
6958 assert_eq!(
6959 python_method_call_count(&py_strategy, py, "on_order_event"),
6960 1
6961 );
6962 let call_names = python_method_call_names(&py_strategy, py);
6963 assert_eq!(
6964 &call_names[call_names.len() - 2..],
6965 ["on_order_accepted", "on_order_event"],
6966 );
6967 });
6968 }
6969
6970 #[rstest::rstest]
6971 #[case("on_position_event")]
6972 #[case("on_position_opened")]
6973 #[case("on_position_changed")]
6974 #[case("on_position_closed")]
6975 fn test_python_dispatch_position_callback_matrix(
6976 #[case] method_name: &str,
6977 #[values(false, true)] raises: bool,
6978 #[values(false, true)] shutdown: bool,
6979 ) {
6980 pyo3::Python::initialize();
6981 Python::attach(|py| {
6982 assert_python_callback(py, method_name, raises, shutdown, |rust_strategy| {
6983 match method_name {
6984 "on_position_event" => {
6985 Strategy::on_position_event(
6986 rust_strategy.inner_mut(),
6987 PositionEvent::PositionOpened(sample_position_opened()),
6988 );
6989 Ok(())
6990 }
6991 "on_position_opened" => {
6992 Strategy::on_position_opened(
6993 rust_strategy.inner_mut(),
6994 sample_position_opened(),
6995 );
6996 Ok(())
6997 }
6998 "on_position_changed" => {
6999 Strategy::on_position_changed(
7000 rust_strategy.inner_mut(),
7001 sample_position_changed(),
7002 );
7003 Ok(())
7004 }
7005 "on_position_closed" => {
7006 Strategy::on_position_closed(
7007 rust_strategy.inner_mut(),
7008 sample_position_closed(),
7009 );
7010 Ok(())
7011 }
7012 _ => unreachable!("unhandled position callback case: {method_name}"),
7013 }
7014 });
7015 });
7016 }
7017
7018 #[rstest::rstest]
7019 fn test_python_handle_position_event_dispatches_specific_and_aggregate_callbacks() {
7020 pyo3::Python::initialize();
7021 Python::attach(|py| {
7022 let (py_strategy, mut rust_strategy) = create_registered_tracking_strategy(py);
7023
7024 rust_strategy.py_start().unwrap();
7025 Strategy::handle_position_event(
7026 rust_strategy.inner_mut(),
7027 PositionEvent::PositionOpened(sample_position_opened()),
7028 );
7029
7030 assert_eq!(
7031 python_method_call_count(&py_strategy, py, "on_position_opened"),
7032 1
7033 );
7034 assert_eq!(
7035 python_method_call_count(&py_strategy, py, "on_position_event"),
7036 1
7037 );
7038 let call_names = python_method_call_names(&py_strategy, py);
7039 assert_eq!(
7040 &call_names[call_names.len() - 2..],
7041 ["on_position_opened", "on_position_event"],
7042 );
7043 });
7044 }
7045
7046 #[rstest::rstest]
7047 fn test_python_self_is_weak() {
7048 Python::initialize();
7049
7050 Python::attach(|py| {
7051 let instance = py
7052 .get_type::<PyStrategy>()
7053 .call0()
7054 .expect("Strategy should construct");
7055 let weakref =
7056 PyWeakrefReference::new(&instance).expect("Strategy should be weak-referenceable");
7057 assert!(weakref.upgrade().is_some());
7058
7059 drop(instance);
7060
7061 assert!(
7063 weakref.upgrade().is_none(),
7064 "an unregistered Strategy must be collected once its last Python owner is dropped",
7065 );
7066 });
7067 }
7068
7069 #[rstest::rstest]
7070 #[case("on_start")]
7071 #[case("on_stop")]
7072 #[case("on_resume")]
7073 #[case("on_reset")]
7074 #[case("on_dispose")]
7075 #[case("on_degrade")]
7076 #[case("on_fault")]
7077 #[case("on_save")]
7078 #[case("on_load")]
7079 #[case("on_time_event")]
7080 #[case("on_data")]
7081 #[case("on_signal")]
7082 #[case("on_queue_state")]
7083 #[case("on_socket_state")]
7084 #[case("on_instrument")]
7085 #[case("on_quote")]
7086 #[case("on_trade")]
7087 #[case("on_bar")]
7088 #[case("on_book_deltas")]
7089 #[case("on_book_depth")]
7090 #[case("on_book")]
7091 #[case("on_mark_price")]
7092 #[case("on_index_price")]
7093 #[case("on_funding_rate")]
7094 #[case("on_instrument_status")]
7095 #[case("on_instrument_close")]
7096 #[case("on_option_greeks")]
7097 #[case("on_option_chain")]
7098 #[case("on_market_exit")]
7099 #[case("post_market_exit")]
7100 #[case("on_order_initialized")]
7101 #[case("on_order_event")]
7102 #[case("on_order_denied")]
7103 #[case("on_order_emulated")]
7104 #[case("on_order_released")]
7105 #[case("on_order_submitted")]
7106 #[case("on_order_rejected")]
7107 #[case("on_order_accepted")]
7108 #[case("on_order_expired")]
7109 #[case("on_order_triggered")]
7110 #[case("on_order_pending_update")]
7111 #[case("on_order_pending_cancel")]
7112 #[case("on_order_modify_rejected")]
7113 #[case("on_order_cancel_rejected")]
7114 #[case("on_order_updated")]
7115 #[case("on_order_canceled")]
7116 #[case("on_order_filled")]
7117 #[case("on_order_fill_voided")]
7118 #[case("on_position_opened")]
7119 #[case("on_position_event")]
7120 #[case("on_position_changed")]
7121 #[case("on_position_closed")]
7122 #[case("on_historical_data")]
7123 #[case("on_historical_book_deltas")]
7124 #[case("on_historical_book_depth")]
7125 #[case("on_historical_quotes")]
7126 #[case("on_historical_trades")]
7127 #[case("on_historical_funding_rates")]
7128 #[case("on_historical_bars")]
7129 #[case("on_historical_mark_prices")]
7130 #[case("on_historical_index_prices")]
7131 fn test_default_callback_during_exclusive_borrow(#[case] method: &str) {
7132 Python::initialize();
7133 Python::attach(|py| {
7134 let argument = match method {
7135 "on_start" | "on_stop" | "on_resume" | "on_reset" | "on_dispose" | "on_degrade"
7136 | "on_fault" | "on_save" | "on_market_exit" | "post_market_exit" => None,
7137 "on_load" => Some(PyDict::new(py).into_any().unbind()),
7138 "on_time_event" => Some(sample_time_event().into_py_any(py).unwrap()),
7139 "on_data" | "on_instrument" | "on_order_event" | "on_position_event"
7140 | "on_historical_data" => Some(py.None()),
7141 "on_signal" => Some(sample_signal().into_py_any(py).unwrap()),
7142 "on_queue_state" => Some(sample_queue_state_changed().into_py_any(py).unwrap()),
7143 "on_socket_state" => Some(sample_socket_state_changed().into_py_any(py).unwrap()),
7144 "on_quote" => Some(sample_quote().into_py_any(py).unwrap()),
7145 "on_trade" => Some(sample_trade().into_py_any(py).unwrap()),
7146 "on_bar" => Some(sample_bar().into_py_any(py).unwrap()),
7147 "on_book_deltas" => Some(sample_book_deltas().into_py_any(py).unwrap()),
7148 "on_book_depth" => Some(sample_book_depth().into_py_any(py).unwrap()),
7149 "on_book" => Some(sample_book().into_py_any(py).unwrap()),
7150 "on_mark_price" => Some(sample_mark_price().into_py_any(py).unwrap()),
7151 "on_index_price" => Some(sample_index_price().into_py_any(py).unwrap()),
7152 "on_funding_rate" => Some(sample_funding_rate().into_py_any(py).unwrap()),
7153 "on_instrument_status" => Some(sample_instrument_status().into_py_any(py).unwrap()),
7154 "on_instrument_close" => Some(sample_instrument_close().into_py_any(py).unwrap()),
7155 "on_option_greeks" => Some(sample_option_greeks().into_py_any(py).unwrap()),
7156 "on_option_chain" => Some(sample_option_chain().into_py_any(py).unwrap()),
7157 "on_order_initialized" => {
7158 Some(OrderInitialized::default().into_py_any(py).unwrap())
7159 }
7160 "on_order_denied" => Some(OrderDenied::default().into_py_any(py).unwrap()),
7161 "on_order_emulated" => Some(OrderEmulated::default().into_py_any(py).unwrap()),
7162 "on_order_released" => Some(OrderReleased::default().into_py_any(py).unwrap()),
7163 "on_order_submitted" => Some(OrderSubmitted::default().into_py_any(py).unwrap()),
7164 "on_order_rejected" => Some(OrderRejected::default().into_py_any(py).unwrap()),
7165 "on_order_accepted" => Some(OrderAccepted::default().into_py_any(py).unwrap()),
7166 "on_order_expired" => Some(OrderExpired::default().into_py_any(py).unwrap()),
7167 "on_order_triggered" => Some(OrderTriggered::default().into_py_any(py).unwrap()),
7168 "on_order_pending_update" => {
7169 Some(OrderPendingUpdate::default().into_py_any(py).unwrap())
7170 }
7171 "on_order_pending_cancel" => {
7172 Some(OrderPendingCancel::default().into_py_any(py).unwrap())
7173 }
7174 "on_order_modify_rejected" => {
7175 Some(OrderModifyRejected::default().into_py_any(py).unwrap())
7176 }
7177 "on_order_cancel_rejected" => {
7178 Some(OrderCancelRejected::default().into_py_any(py).unwrap())
7179 }
7180 "on_order_updated" => Some(OrderUpdated::default().into_py_any(py).unwrap()),
7181 "on_order_canceled" => Some(OrderCanceled::default().into_py_any(py).unwrap()),
7182 "on_order_filled" => {
7183 Some(OrderFilledSpec::builder().build().into_py_any(py).unwrap())
7184 }
7185 "on_order_fill_voided" => Some(
7186 OrderFillVoidedSpec::builder()
7187 .build()
7188 .into_py_any(py)
7189 .unwrap(),
7190 ),
7191 "on_position_opened" => Some(sample_position_opened().into_py_any(py).unwrap()),
7192 "on_position_changed" => Some(sample_position_changed().into_py_any(py).unwrap()),
7193 "on_position_closed" => Some(sample_position_closed().into_py_any(py).unwrap()),
7194 "on_historical_book_deltas"
7195 | "on_historical_book_depth"
7196 | "on_historical_quotes"
7197 | "on_historical_trades"
7198 | "on_historical_funding_rates"
7199 | "on_historical_bars"
7200 | "on_historical_mark_prices"
7201 | "on_historical_index_prices" => Some(PyList::empty(py).into_any().unbind()),
7202 _ => unreachable!(),
7203 };
7204
7205 let instance = py.get_type::<PyStrategy>().call0().unwrap();
7206 let _borrow = instance
7207 .extract::<pyo3::PyRefMut<'_, PyStrategy>>()
7208 .unwrap();
7209
7210 let result = match argument {
7211 Some(argument) => instance.call_method1(method, (argument,)),
7212 None => instance.call_method0(method),
7213 }
7214 .unwrap();
7215
7216 if method == "on_save" {
7217 assert_eq!(result.len().unwrap(), 0);
7218 } else {
7219 assert!(result.is_none());
7220 }
7221 });
7222 }
7223
7224 #[rstest::rstest]
7225 #[case(false)]
7226 #[case(true)]
7227 fn test_modify_order_no_changes_returns_error_without_logging(#[case] same_values: bool) {
7228 Python::initialize();
7229 let _guard = init_logging(
7230 TraderId::from("TRADER-001"),
7231 UUID4::new(),
7232 Default::default(),
7233 Default::default(),
7234 )
7235 .unwrap();
7236 Python::attach(|py| {
7237 let (_, strategy) = create_registered_tracking_strategy(py);
7238 let strategy_id = strategy.strategy_id();
7239 let cache = strategy.inner().core.cache_rc();
7240 let instance = Py::new(py, strategy).unwrap();
7241 let client_order_id = ClientOrderId::from("O-NO-CHANGE-001");
7242 let order = OrderTestBuilder::new(OrderType::Limit)
7243 .instrument_id(InstrumentId::from("AUD/USD.SIM"))
7244 .strategy_id(strategy_id)
7245 .client_order_id(client_order_id)
7246 .price(Price::from("1.23456"))
7247 .quantity(Quantity::from(123))
7248 .submit(true)
7249 .build();
7250 cache
7251 .borrow_mut()
7252 .add_order(order.clone(), None, None, true)
7253 .unwrap();
7254 let argument = client_order_id.into_py_any(py).unwrap();
7255 let quantity = same_values.then_some(order.quantity());
7256 let price = same_values.then_some(order.price().unwrap());
7257 arm_shutdown_on_error(true);
7258 let result = instance.call_method1(py, "modify_order", (argument, quantity, price));
7259 let trigger = take_shutdown_on_error_trigger();
7260 disarm_shutdown_on_error();
7261
7262 let e = result.unwrap_err();
7263 assert!(e.is_instance_of::<pyo3::exceptions::PyRuntimeError>(py));
7264 assert_eq!(
7265 e.to_string(),
7266 "RuntimeError: Cannot create command ModifyOrder: quantity, price, and trigger were either None or the same as existing values"
7267 );
7268 assert_eq!(trigger, None);
7269 assert_eq!(cache.borrow().order(&client_order_id).unwrap(), &order);
7270 });
7271 }
7272
7273 #[rstest::rstest]
7274 #[case(false)]
7275 #[case(true)]
7276 fn test_submit_order_list_validation_returns_error_without_logging(
7277 #[case] duplicate_ids: bool,
7278 ) {
7279 Python::initialize();
7280 let _guard = init_logging(
7281 TraderId::from("TRADER-001"),
7282 UUID4::new(),
7283 Default::default(),
7284 Default::default(),
7285 )
7286 .unwrap();
7287 Python::attach(|py| {
7288 let (_, strategy) = create_registered_tracking_strategy(py);
7289 let client_order_id = ClientOrderId::from("O-LIST-DUPLICATE-001");
7290 let cache = strategy.inner().core.cache_rc();
7291 let order =
7292 sample_python_market_order(py, strategy.strategy_id(), client_order_id).unwrap();
7293
7294 let orders = if duplicate_ids {
7295 vec![order.clone_ref(py), order]
7296 } else {
7297 Vec::new()
7298 };
7299
7300 let instance = Py::new(py, strategy).unwrap();
7301 arm_shutdown_on_error(true);
7302 let result =
7303 instance.call_method1(py, "submit_order_list", (PyList::new(py, orders).unwrap(),));
7304 let trigger = take_shutdown_on_error_trigger();
7305 disarm_shutdown_on_error();
7306
7307 let e = result.unwrap_err();
7308 assert!(e.is_instance_of::<pyo3::exceptions::PyRuntimeError>(py));
7309 assert!(e.to_string().contains(if duplicate_ids {
7310 "duplicate"
7311 } else {
7312 "no orders to submit"
7313 }));
7314 assert_eq!(trigger, None);
7315 assert!(!cache.borrow().order_exists(&client_order_id));
7316 });
7317 }
7318
7319 #[rstest::rstest]
7320 #[case("submit_order", "default")]
7321 #[case("modify_order", "default")]
7322 #[case("submit_order", "super")]
7323 #[case("modify_order", "super")]
7324 #[case("submit_order", "getter")]
7325 #[case("modify_order", "getter")]
7326 fn test_order_command_callback_borrowing(#[case] method: &str, #[case] callback: &str) {
7327 Python::initialize();
7328 let _guard = init_logging(
7329 TraderId::from("TRADER-001"),
7330 UUID4::new(),
7331 Default::default(),
7332 Default::default(),
7333 )
7334 .unwrap();
7335 Python::attach(|py| {
7336 let globals = PyDict::new(py);
7337 globals
7338 .set_item("Strategy", py.get_type::<PyStrategy>())
7339 .unwrap();
7340 py.run(
7341 c_str!(
7342 r#"
7343class CommandStrategy(Strategy):
7344 def __init__(self):
7345 super().__init__()
7346 self.calls = []
7347 self.callback = "default"
7348
7349 def on_start(self):
7350 pass
7351
7352 def on_order_event(self, event):
7353
7354 if self.callback == "getter":
7355 self.calls.append("enter")
7356 self.cache
7357 else:
7358 super().on_order_event(event)
7359 self.calls.append("exit")
7360"#
7361 ),
7362 Some(&globals),
7363 None,
7364 )
7365 .unwrap();
7366 let instance = py
7367 .eval(c_str!("CommandStrategy()"), Some(&globals), None)
7368 .unwrap();
7369 instance.setattr("callback", callback).unwrap();
7370 if callback == "default" {
7371 instance.get_type().delattr("on_order_event").unwrap();
7372 }
7373
7374 let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(VirtualClock::new()));
7375 let cache = Rc::new(RefCell::new(Cache::default()));
7376
7377 let portfolio = Rc::new(RefCell::new(Portfolio::new(
7378 clock.clone(),
7379 cache.clone(),
7380 None,
7381 )));
7382 let strategy_id;
7383 {
7384 let mut strategy = instance
7385 .extract::<pyo3::PyRefMut<'_, PyStrategy>>()
7386 .unwrap();
7387 strategy
7388 .register(
7389 TraderId::from("TRADER-001"),
7390 clock,
7391 cache.clone(),
7392 portfolio,
7393 )
7394 .unwrap();
7395 strategy.register_in_global_registries().unwrap();
7396 Component::start(strategy.inner_mut()).unwrap();
7397 strategy_id = strategy.strategy_id();
7398 }
7399
7400 let actor_id = strategy_id.inner();
7401
7402 let handler = msgbus::TypedHandler::from(move |event: &OrderEventAny| {
7403 let mut strategy = nautilus_common::actor::registry::try_get_actor_unchecked::<
7404 super::PyStrategyInner,
7405 >(&actor_id)
7406 .unwrap();
7407 Strategy::handle_order_event(&mut *strategy, event.clone());
7408 });
7409
7410 let topic = format!("events.order.{strategy_id}");
7411 msgbus::subscribe_order_events(topic.clone().into(), handler.clone(), None);
7412 let (risk_handler, messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
7413 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
7414 msgbus::register_trading_command_endpoint(
7415 MessagingSwitchboard::risk_engine_queue_execute(),
7416 risk_handler,
7417 );
7418 let client_order_id = ClientOrderId::from("O-CALLBACK-001");
7419 let order = OrderTestBuilder::new(OrderType::Limit)
7420 .trader_id(TraderId::from("TRADER-001"))
7421 .strategy_id(strategy_id)
7422 .instrument_id(sample_instrument().id)
7423 .client_order_id(client_order_id)
7424 .price(Price::from("1.23456"))
7425 .quantity(Quantity::from(123))
7426 .submit(method == "modify_order")
7427 .build();
7428
7429 if method == "modify_order" {
7430 cache
7431 .borrow_mut()
7432 .add_order(order.clone(), None, None, true)
7433 .unwrap();
7434 }
7435
7436 arm_shutdown_on_error(true);
7437
7438 let result = if method == "submit_order" {
7439 instance.call_method1(method, (order_any_to_pyobject(py, order).unwrap(),))
7440 } else {
7441 instance.call_method1(method, (client_order_id, Quantity::from(234)))
7442 };
7443
7444 let trigger = take_shutdown_on_error_trigger();
7445 disarm_shutdown_on_error();
7446 msgbus::unsubscribe_order_events(topic.into(), &handler);
7447
7448 assert!(result.unwrap().is_none());
7449 assert_eq!(messages.get_messages().len(), 1);
7450 assert_eq!(
7451 cache.borrow().order(&client_order_id).unwrap().status(),
7452 if method == "submit_order" {
7453 OrderStatus::Initialized
7454 } else {
7455 OrderStatus::PendingUpdate
7456 }
7457 );
7458 let calls = instance
7459 .getattr("calls")
7460 .unwrap()
7461 .extract::<Vec<String>>()
7462 .unwrap();
7463
7464 match callback {
7465 "default" => {
7466 assert_eq!(calls, Vec::<String>::new());
7467 assert_eq!(trigger, None);
7468 }
7469 "super" => {
7470 assert_eq!(calls, ["exit"]);
7471 assert_eq!(trigger, None);
7472 }
7473 "getter" => {
7474 assert_eq!(calls, ["enter"]);
7475 let trigger = trigger.unwrap();
7476 assert_eq!(trigger.component, actor_id);
7477 assert!(
7478 trigger
7479 .message
7480 .starts_with("Python on_order_event failed:\n")
7481 );
7482 assert!(
7483 trigger
7484 .message
7485 .contains("RuntimeError: Already mutably borrowed")
7486 );
7487 }
7488 _ => unreachable!(),
7489 }
7490 });
7491 }
7492}