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