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