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 chrono::{DateTime, Utc};
29use indexmap::IndexMap;
30use nautilus_common::{
31 actor::{
32 Actor, DataActor, DataActorNative,
33 data_actor::DataActorCore,
34 registry::{try_get_actor_unchecked, with_actor_registry},
35 },
36 cache::Cache,
37 clock::Clock,
38 component::{Component, with_component_registry},
39 enums::ComponentState,
40 python::{
41 cache::PyCache,
42 clock::PyClock,
43 config_error_to_pyvalue_err,
44 indicators::{registered_python_indicators, wrap_python_indicator},
45 logging::PyLogger,
46 order_factory::PyOrderFactory,
47 },
48 signal::Signal,
49 timer::{TimeEvent, TimeEventCallback},
50};
51use nautilus_core::{
52 Params, from_pydict,
53 python::{IntoPyObjectNautilusExt, to_pyruntime_err, to_pyvalue_err},
54};
55use nautilus_model::{
56 data::{
57 Bar, BarType, CustomData, DataType, FundingRateUpdate, IndexPriceUpdate, InstrumentStatus,
58 MarkPriceUpdate, OrderBookDeltas, QuoteTick, TradeTick,
59 close::InstrumentClose,
60 option_chain::{OptionChainSlice, OptionGreeks},
61 },
62 enums::{BookType, OmsType, OrderSide, PositionSide, TimeInForce},
63 events::{
64 OrderAccepted, OrderCancelRejected, OrderCanceled, OrderDenied, OrderEmulated,
65 OrderEventAny, OrderExpired, OrderFilled, OrderInitialized, OrderModifyRejected,
66 OrderPendingCancel, OrderPendingUpdate, OrderRejected, OrderReleased, OrderSubmitted,
67 OrderTriggered, OrderUpdated, PositionChanged, PositionClosed, PositionEvent,
68 PositionOpened,
69 },
70 identifiers::{
71 AccountId, ClientId, ClientOrderId, InstrumentId, OptionSeriesId, PositionId, StrategyId,
72 TraderId, Venue,
73 },
74 instruments::InstrumentAny,
75 orderbook::OrderBook,
76 orders::{Order, OrderAny},
77 position::Position,
78 python::{
79 data::option_chain::PyStrikeRange, events::order::order_event_to_pyobject,
80 instruments::instrument_any_to_pyobject, orders::pyobject_to_order_any,
81 },
82 types::{Price, Quantity},
83};
84use nautilus_portfolio::{portfolio::Portfolio, python::PyPortfolio};
85use pyo3::{
86 prelude::*,
87 types::{PyBytes, PyDict, PyList},
88};
89use ustr::Ustr;
90
91use crate::strategy::{
92 BatchModifyOrder, ImportableStrategyConfig, Strategy, StrategyConfig, StrategyCore,
93 StrategyNative,
94};
95
96#[pyo3::pymethods]
97#[pyo3_stub_gen::derive::gen_stub_pymethods]
98impl StrategyConfig {
99 #[new]
101 #[pyo3(signature = (
102 strategy_id=None,
103 order_id_tag=None,
104 oms_type=None,
105 external_order_claims=None,
106 manage_contingent_orders=false,
107 manage_gtd_expiry=false,
108 manage_stop=false,
109 market_exit_interval_ms=100,
110 market_exit_max_attempts=100,
111 market_exit_time_in_force=TimeInForce::Gtc,
112 market_exit_reduce_only=true,
113 use_uuid_client_order_ids=false,
114 use_hyphens_in_client_order_ids=true,
115 log_events=true,
116 log_commands=true,
117 log_rejected_due_post_only_as_warning=true,
118 **_kwargs
119 ))]
120 #[expect(
121 clippy::fn_params_excessive_bools,
122 clippy::too_many_arguments,
123 reason = "constructor mirrors the existing Python keyword API"
124 )]
125 fn py_new(
126 strategy_id: Option<StrategyId>,
127 order_id_tag: Option<String>,
128 oms_type: Option<OmsType>,
129 external_order_claims: Option<Vec<InstrumentId>>,
130 manage_contingent_orders: bool,
131 manage_gtd_expiry: bool,
132 manage_stop: bool,
133 market_exit_interval_ms: u64,
134 market_exit_max_attempts: u64,
135 market_exit_time_in_force: TimeInForce,
136 market_exit_reduce_only: bool,
137 use_uuid_client_order_ids: bool,
138 use_hyphens_in_client_order_ids: bool,
139 log_events: bool,
140 log_commands: bool,
141 log_rejected_due_post_only_as_warning: bool,
142 _kwargs: Option<&Bound<'_, PyDict>>,
143 ) -> PyResult<Self> {
144 let config = Self {
145 strategy_id,
146 order_id_tag,
147 use_uuid_client_order_ids,
148 use_hyphens_in_client_order_ids,
149 oms_type,
150 external_order_claims,
151 manage_contingent_orders,
152 manage_gtd_expiry,
153 manage_stop,
154 market_exit_interval_ms,
155 market_exit_max_attempts,
156 market_exit_time_in_force,
157 market_exit_reduce_only,
158 log_events,
159 log_commands,
160 log_rejected_due_post_only_as_warning,
161 };
162 config.validate().map_err(config_error_to_pyvalue_err)?;
163 Ok(config)
164 }
165
166 #[getter]
167 fn strategy_id(&self) -> Option<StrategyId> {
168 self.strategy_id
169 }
170
171 #[getter]
172 fn order_id_tag(&self) -> Option<&String> {
173 self.order_id_tag.as_ref()
174 }
175
176 #[getter]
177 fn oms_type(&self) -> Option<OmsType> {
178 self.oms_type
179 }
180
181 #[getter]
182 fn manage_contingent_orders(&self) -> bool {
183 self.manage_contingent_orders
184 }
185
186 #[getter]
187 fn manage_gtd_expiry(&self) -> bool {
188 self.manage_gtd_expiry
189 }
190
191 #[getter]
192 fn use_uuid_client_order_ids(&self) -> bool {
193 self.use_uuid_client_order_ids
194 }
195
196 #[getter]
197 fn use_hyphens_in_client_order_ids(&self) -> bool {
198 self.use_hyphens_in_client_order_ids
199 }
200
201 #[getter]
202 fn log_events(&self) -> bool {
203 self.log_events
204 }
205
206 #[getter]
207 fn log_commands(&self) -> bool {
208 self.log_commands
209 }
210
211 #[getter]
212 fn log_rejected_due_post_only_as_warning(&self) -> bool {
213 self.log_rejected_due_post_only_as_warning
214 }
215}
216
217#[pyo3::pymethods]
218#[pyo3_stub_gen::derive::gen_stub_pymethods]
219impl ImportableStrategyConfig {
220 #[new]
222 #[expect(clippy::needless_pass_by_value)]
223 fn py_new(strategy_path: String, config_path: String, config: Py<PyDict>) -> PyResult<Self> {
224 let json_config = Python::attach(|py| -> PyResult<HashMap<String, serde_json::Value>> {
225 let kwargs = PyDict::new(py);
226 kwargs.set_item("default", py.eval(pyo3::ffi::c_str!("str"), None, None)?)?;
227 let json_str: String = PyModule::import(py, "json")?
228 .call_method("dumps", (config.bind(py),), Some(&kwargs))?
229 .extract()?;
230
231 let json_value: serde_json::Value =
232 serde_json::from_str(&json_str).map_err(to_pyvalue_err)?;
233
234 if let serde_json::Value::Object(map) = json_value {
235 Ok(map.into_iter().collect())
236 } else {
237 Err(to_pyvalue_err("Config must be a dictionary"))
238 }
239 })?;
240
241 Ok(Self {
242 strategy_path,
243 config_path,
244 config: json_config,
245 })
246 }
247
248 #[getter]
249 fn strategy_path(&self) -> &String {
250 &self.strategy_path
251 }
252
253 #[getter]
254 fn config_path(&self) -> &String {
255 &self.config_path
256 }
257
258 #[getter]
259 fn config(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
260 let py_dict = PyDict::new(py);
261
262 for (key, value) in &self.config {
263 let json_str = serde_json::to_string(value).map_err(to_pyvalue_err)?;
264 let py_value = PyModule::import(py, "json")?.call_method("loads", (json_str,), None)?;
265 py_dict.set_item(key, py_value)?;
266 }
267 Ok(py_dict.unbind())
268 }
269}
270
271pub struct PyStrategyInner {
273 core: StrategyCore,
274 py_self: Option<Py<PyAny>>,
275 config: Option<Py<PyAny>>,
276 clock: PyClock,
277 logger: PyLogger,
278}
279
280impl Debug for PyStrategyInner {
281 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
282 f.debug_struct(stringify!(PyStrategyInner))
283 .field("core", &self.core)
284 .field("py_self", &self.py_self.as_ref().map(|_| "<Py<PyAny>>"))
285 .field("config", &self.config.as_ref().map(|_| "<Py<PyAny>>"))
286 .field("clock", &self.clock)
287 .field("logger", &self.logger)
288 .finish()
289 }
290}
291
292#[expect(
293 clippy::needless_pass_by_ref_mut,
294 reason = "dispatch methods share receiver shape with mutable DataActor hooks"
295)]
296impl PyStrategyInner {
297 fn dispatch_on_start(&self) -> PyResult<()> {
298 if let Some(ref py_self) = self.py_self {
299 Python::attach(|py| py_self.call_method0(py, "on_start"))?;
300 }
301 Ok(())
302 }
303
304 fn dispatch_on_stop(&self) -> PyResult<()> {
305 if let Some(ref py_self) = self.py_self {
306 Python::attach(|py| py_self.call_method0(py, "on_stop"))?;
307 }
308 Ok(())
309 }
310
311 fn dispatch_on_resume(&self) -> PyResult<()> {
312 if let Some(ref py_self) = self.py_self {
313 Python::attach(|py| py_self.call_method0(py, "on_resume"))?;
314 }
315 Ok(())
316 }
317
318 fn dispatch_on_reset(&self) -> PyResult<()> {
319 if let Some(ref py_self) = self.py_self {
320 Python::attach(|py| py_self.call_method0(py, "on_reset"))?;
321 }
322 Ok(())
323 }
324
325 fn dispatch_on_dispose(&self) -> PyResult<()> {
326 if let Some(ref py_self) = self.py_self {
327 Python::attach(|py| py_self.call_method0(py, "on_dispose"))?;
328 }
329 Ok(())
330 }
331
332 fn dispatch_on_degrade(&self) -> PyResult<()> {
333 if let Some(ref py_self) = self.py_self {
334 Python::attach(|py| py_self.call_method0(py, "on_degrade"))?;
335 }
336 Ok(())
337 }
338
339 fn dispatch_on_fault(&self) -> PyResult<()> {
340 if let Some(ref py_self) = self.py_self {
341 Python::attach(|py| py_self.call_method0(py, "on_fault"))?;
342 }
343 Ok(())
344 }
345
346 fn dispatch_on_save(&self) -> PyResult<IndexMap<String, Vec<u8>>> {
347 if let Some(ref py_self) = self.py_self {
348 Python::attach(|py| {
349 let py_state = py_self.call_method0(py, "on_save")?;
350 let py_state: &Bound<'_, PyDict> = py_state.cast_bound::<PyDict>(py)?;
351 pydict_to_state(py_state)
352 })
353 } else {
354 Ok(IndexMap::new())
355 }
356 }
357
358 fn dispatch_on_load(&self, state: &IndexMap<String, Vec<u8>>) -> PyResult<()> {
359 if let Some(ref py_self) = self.py_self {
360 Python::attach(|py| -> PyResult<()> {
361 let py_state = state_to_pydict(py, state)?;
362 py_self.call_method1(py, "on_load", (py_state,))?;
363 Ok(())
364 })?;
365 }
366 Ok(())
367 }
368
369 fn dispatch_on_market_exit(&self) -> PyResult<()> {
370 if let Some(ref py_self) = self.py_self {
371 Python::attach(|py| py_self.call_method0(py, "on_market_exit"))?;
372 }
373 Ok(())
374 }
375
376 fn dispatch_post_market_exit(&self) -> PyResult<()> {
377 if let Some(ref py_self) = self.py_self {
378 Python::attach(|py| py_self.call_method0(py, "post_market_exit"))?;
379 }
380 Ok(())
381 }
382
383 fn dispatch_on_time_event(&self, event: &TimeEvent) -> PyResult<()> {
384 if let Some(ref py_self) = self.py_self {
385 Python::attach(|py| {
386 py_self.call_method1(py, "on_time_event", (event.clone().into_py_any_unwrap(py),))
387 })?;
388 }
389 Ok(())
390 }
391
392 fn dispatch_on_order_initialized(&self, event: OrderInitialized) -> PyResult<()> {
393 if let Some(ref py_self) = self.py_self {
394 Python::attach(|py| {
395 py_self.call_method1(py, "on_order_initialized", (event.into_py_any_unwrap(py),))
396 })?;
397 }
398 Ok(())
399 }
400
401 fn dispatch_on_order_event(&self, event: OrderEventAny) -> PyResult<()> {
402 if let Some(ref py_self) = self.py_self {
403 Python::attach(|py| {
404 let py_event = order_event_to_pyobject(py, event)?;
405 py_self.call_method1(py, "on_order_event", (py_event,))
406 })?;
407 }
408 Ok(())
409 }
410
411 fn dispatch_on_order_denied(&self, event: OrderDenied) -> PyResult<()> {
412 if let Some(ref py_self) = self.py_self {
413 Python::attach(|py| {
414 py_self.call_method1(py, "on_order_denied", (event.into_py_any_unwrap(py),))
415 })?;
416 }
417 Ok(())
418 }
419
420 fn dispatch_on_order_emulated(&self, event: OrderEmulated) -> PyResult<()> {
421 if let Some(ref py_self) = self.py_self {
422 Python::attach(|py| {
423 py_self.call_method1(py, "on_order_emulated", (event.into_py_any_unwrap(py),))
424 })?;
425 }
426 Ok(())
427 }
428
429 fn dispatch_on_order_released(&self, event: OrderReleased) -> PyResult<()> {
430 if let Some(ref py_self) = self.py_self {
431 Python::attach(|py| {
432 py_self.call_method1(py, "on_order_released", (event.into_py_any_unwrap(py),))
433 })?;
434 }
435 Ok(())
436 }
437
438 fn dispatch_on_order_submitted(&self, event: OrderSubmitted) -> PyResult<()> {
439 if let Some(ref py_self) = self.py_self {
440 Python::attach(|py| {
441 py_self.call_method1(py, "on_order_submitted", (event.into_py_any_unwrap(py),))
442 })?;
443 }
444 Ok(())
445 }
446
447 fn dispatch_on_order_rejected(&self, event: OrderRejected) -> PyResult<()> {
448 if let Some(ref py_self) = self.py_self {
449 Python::attach(|py| {
450 py_self.call_method1(py, "on_order_rejected", (event.into_py_any_unwrap(py),))
451 })?;
452 }
453 Ok(())
454 }
455
456 fn dispatch_on_order_accepted(&self, event: OrderAccepted) -> PyResult<()> {
457 if let Some(ref py_self) = self.py_self {
458 Python::attach(|py| {
459 py_self.call_method1(py, "on_order_accepted", (event.into_py_any_unwrap(py),))
460 })?;
461 }
462 Ok(())
463 }
464
465 fn dispatch_on_order_expired(&self, event: OrderExpired) -> PyResult<()> {
466 if let Some(ref py_self) = self.py_self {
467 Python::attach(|py| {
468 py_self.call_method1(py, "on_order_expired", (event.into_py_any_unwrap(py),))
469 })?;
470 }
471 Ok(())
472 }
473
474 fn dispatch_on_order_triggered(&self, event: OrderTriggered) -> PyResult<()> {
475 if let Some(ref py_self) = self.py_self {
476 Python::attach(|py| {
477 py_self.call_method1(py, "on_order_triggered", (event.into_py_any_unwrap(py),))
478 })?;
479 }
480 Ok(())
481 }
482
483 fn dispatch_on_order_pending_update(&self, event: OrderPendingUpdate) -> PyResult<()> {
484 if let Some(ref py_self) = self.py_self {
485 Python::attach(|py| {
486 py_self.call_method1(
487 py,
488 "on_order_pending_update",
489 (event.into_py_any_unwrap(py),),
490 )
491 })?;
492 }
493 Ok(())
494 }
495
496 fn dispatch_on_order_pending_cancel(&self, event: OrderPendingCancel) -> PyResult<()> {
497 if let Some(ref py_self) = self.py_self {
498 Python::attach(|py| {
499 py_self.call_method1(
500 py,
501 "on_order_pending_cancel",
502 (event.into_py_any_unwrap(py),),
503 )
504 })?;
505 }
506 Ok(())
507 }
508
509 fn dispatch_on_order_modify_rejected(&self, event: OrderModifyRejected) -> PyResult<()> {
510 if let Some(ref py_self) = self.py_self {
511 Python::attach(|py| {
512 py_self.call_method1(
513 py,
514 "on_order_modify_rejected",
515 (event.into_py_any_unwrap(py),),
516 )
517 })?;
518 }
519 Ok(())
520 }
521
522 fn dispatch_on_order_cancel_rejected(&self, event: OrderCancelRejected) -> PyResult<()> {
523 if let Some(ref py_self) = self.py_self {
524 Python::attach(|py| {
525 py_self.call_method1(
526 py,
527 "on_order_cancel_rejected",
528 (event.into_py_any_unwrap(py),),
529 )
530 })?;
531 }
532 Ok(())
533 }
534
535 fn dispatch_on_order_updated(&self, event: &OrderUpdated) -> PyResult<()> {
536 if let Some(ref py_self) = self.py_self {
537 Python::attach(|py| {
538 py_self.call_method1(py, "on_order_updated", ((*event).into_py_any_unwrap(py),))
539 })?;
540 }
541 Ok(())
542 }
543
544 fn dispatch_on_order_canceled(&self, event: OrderCanceled) -> PyResult<()> {
545 if let Some(ref py_self) = self.py_self {
546 Python::attach(|py| {
547 py_self.call_method1(py, "on_order_canceled", (event.into_py_any_unwrap(py),))
548 })?;
549 }
550 Ok(())
551 }
552
553 fn dispatch_on_order_filled(&self, event: &OrderFilled) -> PyResult<()> {
554 if let Some(ref py_self) = self.py_self {
555 Python::attach(|py| {
556 py_self.call_method1(py, "on_order_filled", ((*event).into_py_any_unwrap(py),))
557 })?;
558 }
559 Ok(())
560 }
561
562 fn dispatch_on_position_opened(&self, event: PositionOpened) -> PyResult<()> {
563 if let Some(ref py_self) = self.py_self {
564 Python::attach(|py| {
565 py_self.call_method1(py, "on_position_opened", (event.into_py_any_unwrap(py),))
566 })?;
567 }
568 Ok(())
569 }
570
571 fn dispatch_on_position_event(&self, event: PositionEvent) -> PyResult<()> {
572 if let Some(ref py_self) = self.py_self {
573 Python::attach(|py| {
574 let py_event = match event {
575 PositionEvent::PositionOpened(event) => event.into_py_any_unwrap(py),
576 PositionEvent::PositionChanged(event) => event.into_py_any_unwrap(py),
577 PositionEvent::PositionClosed(event) => event.into_py_any_unwrap(py),
578 PositionEvent::PositionAdjusted(event) => event.into_py_any_unwrap(py),
579 };
580 py_self.call_method1(py, "on_position_event", (py_event,))
581 })?;
582 }
583 Ok(())
584 }
585
586 fn dispatch_on_position_changed(&self, event: PositionChanged) -> PyResult<()> {
587 if let Some(ref py_self) = self.py_self {
588 Python::attach(|py| {
589 py_self.call_method1(py, "on_position_changed", (event.into_py_any_unwrap(py),))
590 })?;
591 }
592 Ok(())
593 }
594
595 fn dispatch_on_position_closed(&self, event: PositionClosed) -> PyResult<()> {
596 if let Some(ref py_self) = self.py_self {
597 Python::attach(|py| {
598 py_self.call_method1(py, "on_position_closed", (event.into_py_any_unwrap(py),))
599 })?;
600 }
601 Ok(())
602 }
603
604 fn dispatch_on_data(&mut self, data: Py<PyAny>) -> PyResult<()> {
605 if let Some(ref py_self) = self.py_self {
606 Python::attach(|py| py_self.call_method1(py, "on_data", (data,)))?;
607 }
608 Ok(())
609 }
610
611 fn dispatch_on_signal(&mut self, signal: &Signal) -> PyResult<()> {
612 if let Some(ref py_self) = self.py_self {
613 Python::attach(|py| {
614 py_self.call_method1(py, "on_signal", (signal.clone().into_py_any_unwrap(py),))
615 })?;
616 }
617 Ok(())
618 }
619
620 fn dispatch_on_instrument(&mut self, instrument: Py<PyAny>) -> PyResult<()> {
621 if let Some(ref py_self) = self.py_self {
622 Python::attach(|py| py_self.call_method1(py, "on_instrument", (instrument,)))?;
623 }
624 Ok(())
625 }
626
627 fn dispatch_on_quote(&mut self, quote: QuoteTick) -> PyResult<()> {
628 if let Some(ref py_self) = self.py_self {
629 Python::attach(|py| {
630 py_self.call_method1(py, "on_quote", (quote.into_py_any_unwrap(py),))
631 })?;
632 }
633 Ok(())
634 }
635
636 fn dispatch_on_trade(&mut self, trade: TradeTick) -> PyResult<()> {
637 if let Some(ref py_self) = self.py_self {
638 Python::attach(|py| {
639 py_self.call_method1(py, "on_trade", (trade.into_py_any_unwrap(py),))
640 })?;
641 }
642 Ok(())
643 }
644
645 fn dispatch_on_bar(&mut self, bar: Bar) -> PyResult<()> {
646 if let Some(ref py_self) = self.py_self {
647 Python::attach(|py| py_self.call_method1(py, "on_bar", (bar.into_py_any_unwrap(py),)))?;
648 }
649 Ok(())
650 }
651
652 fn dispatch_on_book_deltas(&mut self, deltas: &OrderBookDeltas) -> PyResult<()> {
653 if let Some(ref py_self) = self.py_self {
654 Python::attach(|py| {
655 py_self.call_method1(
656 py,
657 "on_book_deltas",
658 (deltas.clone().into_py_any_unwrap(py),),
659 )
660 })?;
661 }
662 Ok(())
663 }
664
665 fn dispatch_on_book(&mut self, book: &OrderBook) -> PyResult<()> {
666 if let Some(ref py_self) = self.py_self {
667 Python::attach(|py| {
668 py_self.call_method1(py, "on_book", (book.clone().into_py_any_unwrap(py),))
669 })?;
670 }
671 Ok(())
672 }
673
674 fn dispatch_on_mark_price(&mut self, mark_price: MarkPriceUpdate) -> PyResult<()> {
675 if let Some(ref py_self) = self.py_self {
676 Python::attach(|py| {
677 py_self.call_method1(py, "on_mark_price", (mark_price.into_py_any_unwrap(py),))
678 })?;
679 }
680 Ok(())
681 }
682
683 fn dispatch_on_index_price(&mut self, index_price: IndexPriceUpdate) -> PyResult<()> {
684 if let Some(ref py_self) = self.py_self {
685 Python::attach(|py| {
686 py_self.call_method1(py, "on_index_price", (index_price.into_py_any_unwrap(py),))
687 })?;
688 }
689 Ok(())
690 }
691
692 fn dispatch_on_funding_rate(&mut self, funding_rate: FundingRateUpdate) -> PyResult<()> {
693 if let Some(ref py_self) = self.py_self {
694 Python::attach(|py| {
695 py_self.call_method1(
696 py,
697 "on_funding_rate",
698 (funding_rate.into_py_any_unwrap(py),),
699 )
700 })?;
701 }
702 Ok(())
703 }
704
705 fn dispatch_on_instrument_status(&mut self, data: InstrumentStatus) -> PyResult<()> {
706 if let Some(ref py_self) = self.py_self {
707 Python::attach(|py| {
708 py_self.call_method1(py, "on_instrument_status", (data.into_py_any_unwrap(py),))
709 })?;
710 }
711 Ok(())
712 }
713
714 fn dispatch_on_instrument_close(&mut self, update: InstrumentClose) -> PyResult<()> {
715 if let Some(ref py_self) = self.py_self {
716 Python::attach(|py| {
717 py_self.call_method1(py, "on_instrument_close", (update.into_py_any_unwrap(py),))
718 })?;
719 }
720 Ok(())
721 }
722
723 fn dispatch_on_option_greeks(&mut self, greeks: OptionGreeks) -> PyResult<()> {
724 if let Some(ref py_self) = self.py_self {
725 Python::attach(|py| {
726 py_self.call_method1(py, "on_option_greeks", (greeks.into_py_any_unwrap(py),))
727 })?;
728 }
729 Ok(())
730 }
731
732 fn dispatch_on_option_chain(&mut self, slice: &OptionChainSlice) -> PyResult<()> {
733 if let Some(ref py_self) = self.py_self {
734 Python::attach(|py| {
735 py_self.call_method1(
736 py,
737 "on_option_chain",
738 (slice.clone().into_py_any_unwrap(py),),
739 )
740 })?;
741 }
742 Ok(())
743 }
744
745 fn dispatch_on_historical_data(&mut self, data: Py<PyAny>) -> PyResult<()> {
746 if let Some(ref py_self) = self.py_self {
747 Python::attach(|py| py_self.call_method1(py, "on_historical_data", (data,)))?;
748 }
749 Ok(())
750 }
751
752 fn dispatch_on_historical_quotes(&mut self, quotes: Vec<QuoteTick>) -> PyResult<()> {
753 if let Some(ref py_self) = self.py_self {
754 Python::attach(|py| {
755 let py_quotes: Vec<_> = quotes
756 .into_iter()
757 .map(|quote| quote.into_py_any_unwrap(py))
758 .collect();
759 py_self.call_method1(py, "on_historical_quotes", (py_quotes,))
760 })?;
761 }
762 Ok(())
763 }
764
765 fn dispatch_on_historical_trades(&mut self, trades: Vec<TradeTick>) -> PyResult<()> {
766 if let Some(ref py_self) = self.py_self {
767 Python::attach(|py| {
768 let py_trades: Vec<_> = trades
769 .into_iter()
770 .map(|trade| trade.into_py_any_unwrap(py))
771 .collect();
772 py_self.call_method1(py, "on_historical_trades", (py_trades,))
773 })?;
774 }
775 Ok(())
776 }
777
778 fn dispatch_on_historical_funding_rates(
779 &mut self,
780 funding_rates: Vec<FundingRateUpdate>,
781 ) -> PyResult<()> {
782 if let Some(ref py_self) = self.py_self {
783 Python::attach(|py| {
784 let py_funding_rates: Vec<_> = funding_rates
785 .into_iter()
786 .map(|rate| rate.into_py_any_unwrap(py))
787 .collect();
788 py_self.call_method1(py, "on_historical_funding_rates", (py_funding_rates,))
789 })?;
790 }
791 Ok(())
792 }
793
794 fn dispatch_on_historical_bars(&mut self, bars: Vec<Bar>) -> PyResult<()> {
795 if let Some(ref py_self) = self.py_self {
796 Python::attach(|py| {
797 let py_bars: Vec<_> = bars
798 .into_iter()
799 .map(|bar| bar.into_py_any_unwrap(py))
800 .collect();
801 py_self.call_method1(py, "on_historical_bars", (py_bars,))
802 })?;
803 }
804 Ok(())
805 }
806
807 fn dispatch_on_historical_mark_prices(
808 &mut self,
809 mark_prices: Vec<MarkPriceUpdate>,
810 ) -> PyResult<()> {
811 if let Some(ref py_self) = self.py_self {
812 Python::attach(|py| {
813 let py_mark_prices: Vec<_> = mark_prices
814 .into_iter()
815 .map(|price| price.into_py_any_unwrap(py))
816 .collect();
817 py_self.call_method1(py, "on_historical_mark_prices", (py_mark_prices,))
818 })?;
819 }
820 Ok(())
821 }
822
823 fn dispatch_on_historical_index_prices(
824 &mut self,
825 index_prices: Vec<IndexPriceUpdate>,
826 ) -> PyResult<()> {
827 if let Some(ref py_self) = self.py_self {
828 Python::attach(|py| {
829 let py_index_prices: Vec<_> = index_prices
830 .into_iter()
831 .map(|price| price.into_py_any_unwrap(py))
832 .collect();
833 py_self.call_method1(py, "on_historical_index_prices", (py_index_prices,))
834 })?;
835 }
836 Ok(())
837 }
838}
839
840impl Deref for PyStrategyInner {
841 type Target = DataActorCore;
842
843 fn deref(&self) -> &Self::Target {
844 DataActorNative::core(&self.core)
845 }
846}
847
848impl DerefMut for PyStrategyInner {
849 fn deref_mut(&mut self) -> &mut Self::Target {
850 DataActorNative::core_mut(&mut self.core)
851 }
852}
853
854impl DataActorNative for PyStrategyInner {
855 fn core(&self) -> &DataActorCore {
856 DataActorNative::core(&self.core)
857 }
858
859 fn core_mut(&mut self) -> &mut DataActorCore {
860 DataActorNative::core_mut(&mut self.core)
861 }
862}
863
864impl StrategyNative for PyStrategyInner {
865 fn strategy_core(&self) -> &StrategyCore {
866 &self.core
867 }
868
869 fn strategy_core_mut(&mut self) -> &mut StrategyCore {
870 &mut self.core
871 }
872}
873
874impl Strategy for PyStrategyInner {
875 fn external_order_claims(&self) -> Option<Vec<InstrumentId>> {
876 self.core.config.external_order_claims.clone()
877 }
878
879 fn on_market_exit(&mut self) {
880 let _ = self.dispatch_on_market_exit();
881 }
882
883 fn post_market_exit(&mut self) {
884 let _ = self.dispatch_post_market_exit();
885 }
886
887 fn on_order_initialized(&mut self, event: OrderInitialized) {
888 let _ = self.dispatch_on_order_initialized(event);
889 }
890
891 fn on_order_event(&mut self, event: OrderEventAny) {
892 let _ = self.dispatch_on_order_event(event);
893 }
894
895 fn on_order_denied(&mut self, event: OrderDenied) {
896 let _ = self.dispatch_on_order_denied(event);
897 }
898
899 fn on_order_emulated(&mut self, event: OrderEmulated) {
900 let _ = self.dispatch_on_order_emulated(event);
901 }
902
903 fn on_order_released(&mut self, event: OrderReleased) {
904 let _ = self.dispatch_on_order_released(event);
905 }
906
907 fn on_order_submitted(&mut self, event: OrderSubmitted) {
908 let _ = self.dispatch_on_order_submitted(event);
909 }
910
911 fn on_order_rejected(&mut self, event: OrderRejected) {
912 let _ = self.dispatch_on_order_rejected(event);
913 }
914
915 fn on_order_accepted(&mut self, event: OrderAccepted) {
916 let _ = self.dispatch_on_order_accepted(event);
917 }
918
919 fn on_order_expired(&mut self, event: OrderExpired) {
920 let _ = self.dispatch_on_order_expired(event);
921 }
922
923 fn on_order_triggered(&mut self, event: OrderTriggered) {
924 let _ = self.dispatch_on_order_triggered(event);
925 }
926
927 fn on_order_pending_update(&mut self, event: OrderPendingUpdate) {
928 let _ = self.dispatch_on_order_pending_update(event);
929 }
930
931 fn on_order_pending_cancel(&mut self, event: OrderPendingCancel) {
932 let _ = self.dispatch_on_order_pending_cancel(event);
933 }
934
935 fn on_order_modify_rejected(&mut self, event: OrderModifyRejected) {
936 let _ = self.dispatch_on_order_modify_rejected(event);
937 }
938
939 fn on_order_cancel_rejected(&mut self, event: OrderCancelRejected) {
940 let _ = self.dispatch_on_order_cancel_rejected(event);
941 }
942
943 fn on_order_updated(&mut self, event: OrderUpdated) {
944 let _ = self.dispatch_on_order_updated(&event);
945 }
946
947 fn on_position_opened(&mut self, event: PositionOpened) {
948 let _ = self.dispatch_on_position_opened(event);
949 }
950
951 fn on_position_event(&mut self, event: PositionEvent) {
952 let _ = self.dispatch_on_position_event(event);
953 }
954
955 fn on_position_changed(&mut self, event: PositionChanged) {
956 let _ = self.dispatch_on_position_changed(event);
957 }
958
959 fn on_position_closed(&mut self, event: PositionClosed) {
960 let _ = self.dispatch_on_position_closed(event);
961 }
962}
963
964impl DataActor for PyStrategyInner {
965 fn on_start(&mut self) -> anyhow::Result<()> {
966 Strategy::on_start(self)?;
967 self.dispatch_on_start()
968 .map_err(|e| anyhow::anyhow!("Python on_start failed: {e}"))
969 }
970
971 fn on_stop(&mut self) -> anyhow::Result<()> {
972 self.dispatch_on_stop()
973 .map_err(|e| anyhow::anyhow!("Python on_stop failed: {e}"))
974 }
975
976 fn on_resume(&mut self) -> anyhow::Result<()> {
977 self.dispatch_on_resume()
978 .map_err(|e| anyhow::anyhow!("Python on_resume failed: {e}"))
979 }
980
981 fn on_reset(&mut self) -> anyhow::Result<()> {
982 self.dispatch_on_reset()
983 .map_err(|e| anyhow::anyhow!("Python on_reset failed: {e}"))
984 }
985
986 fn on_dispose(&mut self) -> anyhow::Result<()> {
987 self.dispatch_on_dispose()
988 .map_err(|e| anyhow::anyhow!("Python on_dispose failed: {e}"))
989 }
990
991 fn on_degrade(&mut self) -> anyhow::Result<()> {
992 self.dispatch_on_degrade()
993 .map_err(|e| anyhow::anyhow!("Python on_degrade failed: {e}"))
994 }
995
996 fn on_fault(&mut self) -> anyhow::Result<()> {
997 self.dispatch_on_fault()
998 .map_err(|e| anyhow::anyhow!("Python on_fault failed: {e}"))
999 }
1000
1001 fn on_save(&self) -> anyhow::Result<IndexMap<String, Vec<u8>>> {
1002 self.dispatch_on_save()
1003 .map_err(|e| anyhow::anyhow!("Python on_save failed: {e}"))
1004 }
1005
1006 fn on_load(&mut self, state: IndexMap<String, Vec<u8>>) -> anyhow::Result<()> {
1007 self.dispatch_on_load(&state)
1008 .map_err(|e| anyhow::anyhow!("Python on_load failed: {e}"))
1009 }
1010
1011 fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()> {
1012 Strategy::on_time_event(self, event)?;
1013 self.dispatch_on_time_event(event)
1014 .map_err(|e| anyhow::anyhow!("Python on_time_event failed: {e}"))
1015 }
1016
1017 #[allow(unused_variables)]
1018 fn on_data(&mut self, data: &CustomData) -> anyhow::Result<()> {
1019 Python::attach(|py| {
1020 let py_data: Py<PyAny> = Py::new(py, data.clone())?.into_any();
1021 self.dispatch_on_data(py_data)
1022 .map_err(|e| anyhow::anyhow!("Python on_data failed: {e}"))
1023 })
1024 }
1025
1026 fn on_signal(&mut self, signal: &Signal) -> anyhow::Result<()> {
1027 self.dispatch_on_signal(signal)
1028 .map_err(|e| anyhow::anyhow!("Python on_signal failed: {e}"))
1029 }
1030
1031 fn on_instrument(&mut self, instrument: &InstrumentAny) -> anyhow::Result<()> {
1032 Python::attach(|py| {
1033 let py_instrument = instrument_any_to_pyobject(py, instrument.clone())
1034 .map_err(|e| anyhow::anyhow!("Failed to convert InstrumentAny to Python: {e}"))?;
1035 self.dispatch_on_instrument(py_instrument)
1036 .map_err(|e| anyhow::anyhow!("Python on_instrument failed: {e}"))
1037 })
1038 }
1039
1040 fn on_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
1041 self.dispatch_on_quote(*quote)
1042 .map_err(|e| anyhow::anyhow!("Python on_quote failed: {e}"))
1043 }
1044
1045 fn on_trade(&mut self, tick: &TradeTick) -> anyhow::Result<()> {
1046 self.dispatch_on_trade(*tick)
1047 .map_err(|e| anyhow::anyhow!("Python on_trade failed: {e}"))
1048 }
1049
1050 fn on_bar(&mut self, bar: &Bar) -> anyhow::Result<()> {
1051 self.dispatch_on_bar(*bar)
1052 .map_err(|e| anyhow::anyhow!("Python on_bar failed: {e}"))
1053 }
1054
1055 fn on_book_deltas(&mut self, deltas: &OrderBookDeltas) -> anyhow::Result<()> {
1056 self.dispatch_on_book_deltas(deltas)
1057 .map_err(|e| anyhow::anyhow!("Python on_book_deltas failed: {e}"))
1058 }
1059
1060 fn on_book(&mut self, order_book: &OrderBook) -> anyhow::Result<()> {
1061 self.dispatch_on_book(order_book)
1062 .map_err(|e| anyhow::anyhow!("Python on_book failed: {e}"))
1063 }
1064
1065 fn on_mark_price(&mut self, mark_price: &MarkPriceUpdate) -> anyhow::Result<()> {
1066 self.dispatch_on_mark_price(*mark_price)
1067 .map_err(|e| anyhow::anyhow!("Python on_mark_price failed: {e}"))
1068 }
1069
1070 fn on_index_price(&mut self, index_price: &IndexPriceUpdate) -> anyhow::Result<()> {
1071 self.dispatch_on_index_price(*index_price)
1072 .map_err(|e| anyhow::anyhow!("Python on_index_price failed: {e}"))
1073 }
1074
1075 fn on_funding_rate(&mut self, funding_rate: &FundingRateUpdate) -> anyhow::Result<()> {
1076 self.dispatch_on_funding_rate(*funding_rate)
1077 .map_err(|e| anyhow::anyhow!("Python on_funding_rate failed: {e}"))
1078 }
1079
1080 fn on_instrument_status(&mut self, data: &InstrumentStatus) -> anyhow::Result<()> {
1081 self.dispatch_on_instrument_status(*data)
1082 .map_err(|e| anyhow::anyhow!("Python on_instrument_status failed: {e}"))
1083 }
1084
1085 fn on_instrument_close(&mut self, update: &InstrumentClose) -> anyhow::Result<()> {
1086 self.dispatch_on_instrument_close(*update)
1087 .map_err(|e| anyhow::anyhow!("Python on_instrument_close failed: {e}"))
1088 }
1089
1090 fn on_option_greeks(&mut self, greeks: &OptionGreeks) -> anyhow::Result<()> {
1091 self.dispatch_on_option_greeks(*greeks)
1092 .map_err(|e| anyhow::anyhow!("Python on_option_greeks failed: {e}"))
1093 }
1094
1095 fn on_option_chain(&mut self, slice: &OptionChainSlice) -> anyhow::Result<()> {
1096 self.dispatch_on_option_chain(slice)
1097 .map_err(|e| anyhow::anyhow!("Python on_option_chain failed: {e}"))
1098 }
1099
1100 fn on_historical_data(&mut self, data: &dyn Any) -> anyhow::Result<()> {
1101 Python::attach(|py| {
1102 let py_data: Py<PyAny> = if let Some(custom_data) = data.downcast_ref::<CustomData>() {
1103 Py::new(py, custom_data.clone())?.into_any()
1104 } else {
1105 anyhow::bail!("Failed to convert historical data to Python: unsupported type");
1106 };
1107 self.dispatch_on_historical_data(py_data)
1108 .map_err(|e| anyhow::anyhow!("Python on_historical_data failed: {e}"))
1109 })
1110 }
1111
1112 fn on_historical_quotes(&mut self, quotes: &[QuoteTick]) -> anyhow::Result<()> {
1113 self.dispatch_on_historical_quotes(quotes.to_vec())
1114 .map_err(|e| anyhow::anyhow!("Python on_historical_quotes failed: {e}"))
1115 }
1116
1117 fn on_historical_trades(&mut self, trades: &[TradeTick]) -> anyhow::Result<()> {
1118 self.dispatch_on_historical_trades(trades.to_vec())
1119 .map_err(|e| anyhow::anyhow!("Python on_historical_trades failed: {e}"))
1120 }
1121
1122 fn on_historical_funding_rates(
1123 &mut self,
1124 funding_rates: &[FundingRateUpdate],
1125 ) -> anyhow::Result<()> {
1126 self.dispatch_on_historical_funding_rates(funding_rates.to_vec())
1127 .map_err(|e| anyhow::anyhow!("Python on_historical_funding_rates failed: {e}"))
1128 }
1129
1130 fn on_historical_bars(&mut self, bars: &[Bar]) -> anyhow::Result<()> {
1131 self.dispatch_on_historical_bars(bars.to_vec())
1132 .map_err(|e| anyhow::anyhow!("Python on_historical_bars failed: {e}"))
1133 }
1134
1135 fn on_historical_mark_prices(&mut self, mark_prices: &[MarkPriceUpdate]) -> anyhow::Result<()> {
1136 self.dispatch_on_historical_mark_prices(mark_prices.to_vec())
1137 .map_err(|e| anyhow::anyhow!("Python on_historical_mark_prices failed: {e}"))
1138 }
1139
1140 fn on_historical_index_prices(
1141 &mut self,
1142 index_prices: &[IndexPriceUpdate],
1143 ) -> anyhow::Result<()> {
1144 self.dispatch_on_historical_index_prices(index_prices.to_vec())
1145 .map_err(|e| anyhow::anyhow!("Python on_historical_index_prices failed: {e}"))
1146 }
1147
1148 fn on_order_filled(&mut self, event: &OrderFilled) -> anyhow::Result<()> {
1149 self.dispatch_on_order_filled(event)
1150 .map_err(|e| anyhow::anyhow!("Python on_order_filled failed: {e}"))
1151 }
1152
1153 fn on_order_canceled(&mut self, event: &OrderCanceled) -> anyhow::Result<()> {
1154 self.dispatch_on_order_canceled(*event)
1155 .map_err(|e| anyhow::anyhow!("Python on_order_canceled failed: {e}"))
1156 }
1157}
1158
1159fn state_to_pydict(py: Python<'_>, state: &IndexMap<String, Vec<u8>>) -> PyResult<Py<PyDict>> {
1160 let py_state = PyDict::new(py);
1161 for (key, value) in state {
1162 py_state.set_item(key, PyBytes::new(py, value))?;
1163 }
1164 Ok(py_state.unbind())
1165}
1166
1167fn pydict_to_state(state: &Bound<'_, PyDict>) -> PyResult<IndexMap<String, Vec<u8>>> {
1168 let mut rust_state = IndexMap::with_capacity(state.len());
1169 for (key, value) in state.iter() {
1170 rust_state.insert(key.extract()?, value.extract()?);
1171 }
1172 Ok(rust_state)
1173}
1174
1175#[allow(non_camel_case_types)]
1177#[pyo3::pyclass(
1178 module = "nautilus_trader.trading",
1179 name = "Strategy",
1180 unsendable,
1181 subclass
1182)]
1183#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.trading")]
1184pub struct PyStrategy {
1185 inner: Rc<UnsafeCell<PyStrategyInner>>,
1186}
1187
1188impl Debug for PyStrategy {
1189 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1190 f.debug_struct(stringify!(PyStrategy))
1191 .field("inner", &self.inner())
1192 .finish()
1193 }
1194}
1195
1196impl PyStrategy {
1197 #[inline]
1198 #[allow(unsafe_code)]
1199 pub(crate) fn inner(&self) -> &PyStrategyInner {
1200 unsafe { &*self.inner.get() }
1203 }
1204
1205 #[inline]
1206 #[allow(unsafe_code, clippy::mut_from_ref)]
1207 pub(crate) fn inner_mut(&self) -> &mut PyStrategyInner {
1208 unsafe { &mut *self.inner.get() }
1211 }
1212}
1213
1214impl PyStrategy {
1215 #[must_use]
1217 pub fn new(config: Option<StrategyConfig>) -> Self {
1218 let config = config.unwrap_or_default();
1219 let core = StrategyCore::new(config);
1220 let clock = PyClock::new_test();
1221 let logger = PyLogger::new(core.actor.actor_id.as_str());
1222
1223 let inner = PyStrategyInner {
1224 core,
1225 py_self: None,
1226 config: None,
1227 clock,
1228 logger,
1229 };
1230
1231 Self {
1232 inner: Rc::new(UnsafeCell::new(inner)),
1233 }
1234 }
1235
1236 pub fn set_python_instance(&mut self, py_obj: Py<PyAny>) {
1238 self.inner_mut().py_self = Some(py_obj);
1239 }
1240
1241 pub fn set_config(&mut self, config: Option<Py<PyAny>>) {
1247 self.inner_mut().config = config;
1248 }
1249
1250 pub fn set_external_order_claims(&mut self, external_order_claims: Option<Vec<InstrumentId>>) {
1252 self.inner_mut().core.config.external_order_claims = external_order_claims;
1253 }
1254
1255 #[must_use]
1257 pub fn external_order_claims(&self) -> Option<Vec<InstrumentId>> {
1258 self.inner().external_order_claims()
1259 }
1260
1261 pub fn set_strategy_id(&mut self, strategy_id: StrategyId) -> anyhow::Result<()> {
1265 let inner = self.inner_mut();
1266 inner.core.change_id(strategy_id);
1267 Ok(())
1268 }
1269
1270 pub fn set_order_id_tag(&mut self, order_id_tag: &str) -> anyhow::Result<()> {
1272 let inner = self.inner_mut();
1273 inner.core.change_order_id_tag(order_id_tag);
1274 Ok(())
1275 }
1276
1277 pub fn set_log_events(&mut self, log_events: bool) {
1279 let inner = self.inner_mut();
1280 inner.core.actor.config.log_events = log_events;
1281 }
1282
1283 pub fn set_log_commands(&mut self, log_commands: bool) {
1285 let inner = self.inner_mut();
1286 inner.core.actor.config.log_commands = log_commands;
1287 }
1288
1289 #[must_use]
1291 pub fn strategy_id(&self) -> StrategyId {
1292 StrategyId::from(self.inner().core.actor.actor_id.inner().as_str())
1293 }
1294
1295 #[must_use]
1297 pub fn is_registered(&self) -> bool {
1298 self.inner().core.actor.is_registered()
1299 }
1300
1301 pub fn register(
1307 &mut self,
1308 trader_id: TraderId,
1309 clock: Rc<RefCell<dyn Clock>>,
1310 cache: Rc<RefCell<Cache>>,
1311 portfolio: Rc<RefCell<Portfolio>>,
1312 ) -> anyhow::Result<()> {
1313 let inner = self.inner_mut();
1314 inner.core.register(trader_id, clock, cache, portfolio)?;
1315
1316 inner.clock = PyClock::from_rc(inner.core.actor.clock_rc());
1317
1318 let actor_id = inner.core.actor.actor_id.inner();
1319 let callback = TimeEventCallback::from(move |event: TimeEvent| {
1320 if let Some(mut strategy) = try_get_actor_unchecked::<PyStrategyInner>(&actor_id) {
1321 if let Err(e) = DataActor::on_time_event(&mut *strategy, &event) {
1322 log::error!("Python time event handler failed for strategy {actor_id}: {e}");
1323 }
1324 } else {
1325 log::error!("Strategy {actor_id} not found for time event handling");
1326 }
1327 });
1328
1329 inner.clock.inner_mut().register_default_handler(callback);
1330
1331 Component::initialize(inner)
1332 }
1333
1334 pub fn register_in_global_registries(&self) {
1336 let inner = self.inner();
1337 let component_id = Component::component_id(inner).inner();
1338 let actor_id = Actor::id(inner);
1339
1340 let inner_ref: Rc<UnsafeCell<PyStrategyInner>> = self.inner.clone();
1341
1342 let component_trait_ref: Rc<UnsafeCell<dyn Component>> = inner_ref.clone();
1343 with_component_registry(|registry| registry.insert(component_id, component_trait_ref));
1344
1345 let actor_trait_ref: Rc<UnsafeCell<dyn Actor>> = inner_ref;
1346 with_actor_registry(|registry| registry.insert(actor_id, actor_trait_ref));
1347 }
1348}
1349
1350#[pyo3::pymethods]
1351#[pyo3_stub_gen::derive::gen_stub_pymethods]
1352#[expect(
1353 clippy::large_types_passed_by_value,
1354 clippy::unused_self,
1355 reason = "default PyO3 callbacks must remain instance methods and accept Python-owned event values"
1356)]
1357impl PyStrategy {
1358 #[new]
1370 #[pyo3(signature = (config=None))]
1371 fn py_new(config: Option<Py<PyAny>>) -> Self {
1372 let strategy_config = config
1373 .as_ref()
1374 .and_then(|obj| Python::attach(|py| obj.extract::<StrategyConfig>(py).ok()));
1375 let mut strategy = Self::new(strategy_config);
1376 strategy.set_config(config);
1377 strategy
1378 }
1379
1380 #[pyo3(signature = (config=None))]
1382 fn __init__(slf: &Bound<'_, Self>, config: Option<Py<PyAny>>) {
1383 let py_self: Py<PyAny> = slf.clone().unbind().into_any();
1384 let mut borrowed = slf.borrow_mut();
1385 borrowed.set_python_instance(py_self);
1386 if config.is_some() {
1388 borrowed.set_config(config);
1389 }
1390 }
1391
1392 #[getter]
1393 #[pyo3(name = "trader_id")]
1394 fn py_trader_id(&self) -> Option<TraderId> {
1395 self.inner().core.trader_id()
1396 }
1397
1398 #[getter]
1399 #[pyo3(name = "strategy_id")]
1400 fn py_strategy_id(&self) -> StrategyId {
1401 StrategyId::from(self.inner().core.actor.actor_id.inner().as_str())
1402 }
1403
1404 #[getter]
1405 #[pyo3(name = "config")]
1406 fn py_config(&self, py: Python<'_>) -> Option<Py<PyAny>> {
1407 self.inner()
1408 .config
1409 .as_ref()
1410 .map(|config| config.clone_ref(py))
1411 }
1412
1413 #[getter]
1414 #[pyo3(name = "clock")]
1415 fn py_clock(&self) -> PyResult<PyClock> {
1416 let inner = self.inner();
1417 if inner.core.actor.is_registered() {
1418 Ok(inner.clock.clone())
1419 } else {
1420 Err(to_pyruntime_err(
1421 "Strategy must be registered with a trader before accessing clock",
1422 ))
1423 }
1424 }
1425
1426 #[getter]
1427 #[pyo3(name = "cache")]
1428 fn py_cache(&self) -> PyResult<PyCache> {
1429 let inner = self.inner();
1430 if inner.core.actor.is_registered() {
1431 Ok(PyCache::from_rc(inner.core.actor.cache_rc()))
1432 } else {
1433 Err(to_pyruntime_err(
1434 "Strategy must be registered with a trader before accessing cache",
1435 ))
1436 }
1437 }
1438
1439 #[getter]
1440 #[pyo3(name = "portfolio")]
1441 fn py_portfolio(&self) -> PyResult<PyPortfolio> {
1442 let inner = self.inner();
1443 if inner.core.actor.is_registered() {
1444 Ok(PyPortfolio::from_rc(inner.portfolio_rc()))
1445 } else {
1446 Err(to_pyruntime_err(
1447 "Strategy must be registered with a trader before accessing portfolio",
1448 ))
1449 }
1450 }
1451
1452 #[getter]
1453 #[pyo3(name = "order_factory")]
1454 fn py_order_factory(&self) -> PyResult<PyOrderFactory> {
1455 let inner = self.inner();
1456 if inner.core.actor.is_registered() {
1457 Ok(PyOrderFactory::from_rc(inner.order_factory_rc()))
1458 } else {
1459 Err(to_pyruntime_err(
1460 "Strategy must be registered with a trader before accessing order_factory",
1461 ))
1462 }
1463 }
1464
1465 #[getter]
1466 #[pyo3(name = "log")]
1467 fn py_log(&self) -> PyLogger {
1468 self.inner().logger.clone()
1469 }
1470
1471 #[pyo3(name = "state")]
1472 fn py_state(&self) -> ComponentState {
1473 self.inner().core.actor.state()
1474 }
1475
1476 #[pyo3(name = "is_ready")]
1477 fn py_is_ready(&self) -> bool {
1478 Component::is_ready(self.inner())
1479 }
1480
1481 #[pyo3(name = "is_running")]
1482 fn py_is_running(&self) -> bool {
1483 Component::is_running(self.inner())
1484 }
1485
1486 #[pyo3(name = "is_stopped")]
1487 fn py_is_stopped(&self) -> bool {
1488 Component::is_stopped(self.inner())
1489 }
1490
1491 #[pyo3(name = "is_disposed")]
1492 fn py_is_disposed(&self) -> bool {
1493 Component::is_disposed(self.inner())
1494 }
1495
1496 #[pyo3(name = "is_degraded")]
1497 fn py_is_degraded(&self) -> bool {
1498 Component::is_degraded(self.inner())
1499 }
1500
1501 #[pyo3(name = "is_faulted")]
1502 fn py_is_faulted(&self) -> bool {
1503 Component::is_faulted(self.inner())
1504 }
1505
1506 #[pyo3(name = "start")]
1507 fn py_start(&mut self) -> PyResult<()> {
1508 Component::start(self.inner_mut()).map_err(to_pyruntime_err)
1509 }
1510
1511 #[pyo3(name = "stop")]
1512 fn py_stop(&mut self) -> PyResult<()> {
1513 let inner = self.inner_mut();
1514 if Strategy::stop(inner) {
1515 Component::stop(inner).map_err(to_pyruntime_err)
1516 } else {
1517 Ok(())
1518 }
1519 }
1520
1521 #[pyo3(name = "market_exit")]
1522 fn py_market_exit(&mut self) -> PyResult<()> {
1523 Strategy::market_exit(self.inner_mut()).map_err(to_pyruntime_err)
1524 }
1525
1526 #[pyo3(name = "is_exiting")]
1527 fn py_is_exiting(&self) -> bool {
1528 Strategy::is_exiting(self.inner())
1529 }
1530
1531 #[pyo3(name = "save")]
1532 fn py_save(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
1533 let state = DataActor::on_save(self.inner()).map_err(to_pyruntime_err)?;
1534 state_to_pydict(py, &state)
1535 }
1536
1537 #[pyo3(name = "load")]
1538 fn py_load(&mut self, state: &Bound<'_, PyDict>) -> PyResult<()> {
1539 let state = pydict_to_state(state)?;
1540 DataActor::on_load(self.inner_mut(), state).map_err(to_pyruntime_err)
1541 }
1542
1543 #[pyo3(name = "resume")]
1544 fn py_resume(&mut self) -> PyResult<()> {
1545 Component::resume(self.inner_mut()).map_err(to_pyruntime_err)
1546 }
1547
1548 #[pyo3(name = "reset")]
1549 fn py_reset(&mut self) -> PyResult<()> {
1550 Component::reset(self.inner_mut()).map_err(to_pyruntime_err)
1551 }
1552
1553 #[pyo3(name = "dispose")]
1554 fn py_dispose(&mut self) -> PyResult<()> {
1555 Component::dispose(self.inner_mut()).map_err(to_pyruntime_err)
1556 }
1557
1558 #[pyo3(name = "degrade")]
1559 fn py_degrade(&mut self) -> PyResult<()> {
1560 Component::degrade(self.inner_mut()).map_err(to_pyruntime_err)
1561 }
1562
1563 #[pyo3(name = "fault")]
1564 fn py_fault(&mut self) -> PyResult<()> {
1565 Component::fault(self.inner_mut()).map_err(to_pyruntime_err)
1566 }
1567
1568 #[getter]
1569 #[pyo3(name = "registered_indicators")]
1570 fn py_registered_indicators(&self, py: Python<'_>) -> PyResult<Py<PyList>> {
1571 let inner = self.inner();
1572 registered_python_indicators(
1573 py,
1574 DataActorNative::core(&inner.core).registered_indicators(),
1575 )
1576 }
1577
1578 #[pyo3(name = "indicators_initialized")]
1579 fn py_indicators_initialized(&self, _py: Python<'_>) -> PyResult<bool> {
1580 let inner = self.inner();
1581 DataActorNative::core(&inner.core)
1582 .indicators_initialized()
1583 .map_err(to_pyruntime_err)
1584 }
1585
1586 #[pyo3(name = "register_indicator_for_quote_ticks")]
1587 fn py_register_indicator_for_quote_ticks(
1588 &mut self,
1589 py: Python<'_>,
1590 instrument_id: InstrumentId,
1591 indicator: Py<PyAny>,
1592 ) {
1593 let indicator = wrap_python_indicator(py, indicator);
1594 let inner = self.inner_mut();
1595 DataActorNative::core_mut(&mut inner.core)
1596 .register_indicator_for_quote_ticks(instrument_id, indicator);
1597 }
1598
1599 #[pyo3(name = "register_indicator_for_trade_ticks")]
1600 fn py_register_indicator_for_trade_ticks(
1601 &mut self,
1602 py: Python<'_>,
1603 instrument_id: InstrumentId,
1604 indicator: Py<PyAny>,
1605 ) {
1606 let indicator = wrap_python_indicator(py, indicator);
1607 let inner = self.inner_mut();
1608 DataActorNative::core_mut(&mut inner.core)
1609 .register_indicator_for_trade_ticks(instrument_id, indicator);
1610 }
1611
1612 #[pyo3(name = "register_indicator_for_bars")]
1613 fn py_register_indicator_for_bars(
1614 &mut self,
1615 py: Python<'_>,
1616 bar_type: BarType,
1617 indicator: Py<PyAny>,
1618 ) {
1619 let indicator = wrap_python_indicator(py, indicator);
1620 let inner = self.inner_mut();
1621 DataActorNative::core_mut(&mut inner.core).register_indicator_for_bars(bar_type, indicator);
1622 }
1623
1624 #[pyo3(name = "submit_order")]
1625 #[pyo3(signature = (order, position_id=None, client_id=None, params=None))]
1626 fn py_submit_order(
1627 &mut self,
1628 py: Python<'_>,
1629 order: Py<PyAny>,
1630 position_id: Option<PositionId>,
1631 client_id: Option<ClientId>,
1632 params: Option<Py<PyDict>>,
1633 ) -> PyResult<()> {
1634 let order = pyobject_to_order_any(py, order)?;
1635 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
1636 match params {
1637 Some(dict) => from_pydict(py, &dict),
1638 None => Ok(None),
1639 }
1640 })?;
1641 let inner = self.inner_mut();
1642
1643 Strategy::submit_order(inner, order, position_id, client_id, params_map)
1644 .map_err(to_pyruntime_err)
1645 }
1646
1647 #[pyo3(name = "submit_order_list")]
1648 #[pyo3(signature = (order_list, position_id=None, client_id=None, params=None))]
1649 #[expect(
1650 clippy::needless_pass_by_value,
1651 reason = "PyO3 owns extracted method arguments before Rust conversion"
1652 )]
1653 fn py_submit_order_list(
1654 &mut self,
1655 py: Python<'_>,
1656 order_list: Py<PyAny>,
1657 position_id: Option<PositionId>,
1658 client_id: Option<ClientId>,
1659 params: Option<Py<PyDict>>,
1660 ) -> PyResult<()> {
1661 let orders = py_order_list_to_orders(py, &order_list)?;
1662 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
1663 match params {
1664 Some(dict) => from_pydict(py, &dict),
1665 None => Ok(None),
1666 }
1667 })?;
1668 let inner = self.inner_mut();
1669
1670 Strategy::submit_order_list(inner, orders, position_id, client_id, params_map)
1671 .map_err(to_pyruntime_err)
1672 }
1673
1674 #[pyo3(name = "modify_order")]
1675 #[pyo3(signature = (client_order_id, quantity=None, price=None, trigger_price=None, client_id=None, params=None))]
1676 fn py_modify_order(
1677 &mut self,
1678 client_order_id: ClientOrderId,
1679 quantity: Option<Quantity>,
1680 price: Option<Price>,
1681 trigger_price: Option<Price>,
1682 client_id: Option<ClientId>,
1683 params: Option<Py<PyDict>>,
1684 ) -> PyResult<()> {
1685 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
1686 match params {
1687 Some(dict) => from_pydict(py, &dict),
1688 None => Ok(None),
1689 }
1690 })?;
1691 let inner = self.inner_mut();
1692
1693 Strategy::modify_order(
1694 inner,
1695 client_order_id,
1696 quantity,
1697 price,
1698 trigger_price,
1699 client_id,
1700 params_map,
1701 )
1702 .map_err(to_pyruntime_err)
1703 }
1704
1705 #[pyo3(name = "modify_orders")]
1706 #[pyo3(signature = (updates, client_id=None, params=None))]
1707 fn py_modify_orders(
1708 &mut self,
1709 updates: Vec<BatchModifyOrder>,
1710 client_id: Option<ClientId>,
1711 params: Option<Py<PyDict>>,
1712 ) -> PyResult<()> {
1713 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
1714 match params {
1715 Some(dict) => from_pydict(py, &dict),
1716 None => Ok(None),
1717 }
1718 })?;
1719
1720 Strategy::modify_orders(self.inner_mut(), updates, client_id, params_map)
1721 .map_err(to_pyruntime_err)
1722 }
1723
1724 #[pyo3(name = "cancel_order")]
1725 #[pyo3(signature = (client_order_id, client_id=None, params=None))]
1726 fn py_cancel_order(
1727 &mut self,
1728 client_order_id: ClientOrderId,
1729 client_id: Option<ClientId>,
1730 params: Option<Py<PyDict>>,
1731 ) -> PyResult<()> {
1732 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
1733 match params {
1734 Some(dict) => from_pydict(py, &dict),
1735 None => Ok(None),
1736 }
1737 })?;
1738 let inner = self.inner_mut();
1739
1740 Strategy::cancel_order(inner, client_order_id, client_id, params_map)
1741 .map_err(to_pyruntime_err)
1742 }
1743
1744 #[pyo3(name = "cancel_gtd_expiry")]
1746 #[pyo3(signature = (order))]
1747 fn py_cancel_gtd_expiry(&mut self, py: Python<'_>, order: Py<PyAny>) -> PyResult<()> {
1748 let order = pyobject_to_order_any(py, order)?;
1749
1750 Strategy::cancel_gtd_expiry(self.inner_mut(), &order.client_order_id());
1751 Ok(())
1752 }
1753
1754 #[pyo3(name = "cancel_orders")]
1755 #[pyo3(signature = (client_order_ids, client_id=None, params=None))]
1756 fn py_cancel_orders(
1757 &mut self,
1758 client_order_ids: Vec<ClientOrderId>,
1759 client_id: Option<ClientId>,
1760 params: Option<Py<PyDict>>,
1761 ) -> PyResult<()> {
1762 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
1763 match params {
1764 Some(dict) => from_pydict(py, &dict),
1765 None => Ok(None),
1766 }
1767 })?;
1768
1769 Strategy::cancel_orders(self.inner_mut(), client_order_ids, client_id, params_map)
1770 .map_err(to_pyruntime_err)
1771 }
1772
1773 #[pyo3(name = "cancel_all_orders")]
1774 #[pyo3(signature = (instrument_id, order_side=None, client_id=None, params=None))]
1775 fn py_cancel_all_orders(
1776 &mut self,
1777 instrument_id: InstrumentId,
1778 order_side: Option<OrderSide>,
1779 client_id: Option<ClientId>,
1780 params: Option<Py<PyDict>>,
1781 ) -> PyResult<()> {
1782 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
1783 match params {
1784 Some(dict) => from_pydict(py, &dict),
1785 None => Ok(None),
1786 }
1787 })?;
1788 Strategy::cancel_all_orders(
1789 self.inner_mut(),
1790 instrument_id,
1791 order_side,
1792 client_id,
1793 params_map,
1794 )
1795 .map_err(to_pyruntime_err)
1796 }
1797
1798 #[pyo3(name = "close_position")]
1799 #[pyo3(signature = (position, client_id=None, tags=None, time_in_force=None, reduce_only=None, quote_quantity=None))]
1800 fn py_close_position(
1801 &mut self,
1802 position: &Position,
1803 client_id: Option<ClientId>,
1804 tags: Option<Vec<String>>,
1805 time_in_force: Option<TimeInForce>,
1806 reduce_only: Option<bool>,
1807 quote_quantity: Option<bool>,
1808 ) -> PyResult<()> {
1809 let tags = tags.map(|t| t.into_iter().map(|s| Ustr::from(&s)).collect());
1810 Strategy::close_position(
1811 self.inner_mut(),
1812 position,
1813 client_id,
1814 tags,
1815 time_in_force,
1816 reduce_only,
1817 quote_quantity,
1818 )
1819 .map_err(to_pyruntime_err)
1820 }
1821
1822 #[pyo3(name = "close_all_positions")]
1823 #[pyo3(signature = (instrument_id, position_side=None, client_id=None, tags=None, time_in_force=None, reduce_only=None, quote_quantity=None))]
1824 #[expect(clippy::too_many_arguments)]
1825 fn py_close_all_positions(
1826 &mut self,
1827 instrument_id: InstrumentId,
1828 position_side: Option<PositionSide>,
1829 client_id: Option<ClientId>,
1830 tags: Option<Vec<String>>,
1831 time_in_force: Option<TimeInForce>,
1832 reduce_only: Option<bool>,
1833 quote_quantity: Option<bool>,
1834 ) -> PyResult<()> {
1835 let tags = tags.map(|t| t.into_iter().map(|s| Ustr::from(&s)).collect());
1836 Strategy::close_all_positions(
1837 self.inner_mut(),
1838 instrument_id,
1839 position_side,
1840 client_id,
1841 tags,
1842 time_in_force,
1843 reduce_only,
1844 quote_quantity,
1845 )
1846 .map_err(to_pyruntime_err)
1847 }
1848
1849 #[pyo3(name = "query_account")]
1850 #[pyo3(signature = (account_id, client_id=None, params=None))]
1851 fn py_query_account(
1852 &mut self,
1853 py: Python<'_>,
1854 account_id: AccountId,
1855 client_id: Option<ClientId>,
1856 params: Option<Py<PyDict>>,
1857 ) -> PyResult<()> {
1858 let params_map = match params {
1859 Some(dict) => from_pydict(py, &dict)?,
1860 None => None,
1861 };
1862 Strategy::query_account(self.inner_mut(), account_id, client_id, params_map)
1863 .map_err(to_pyruntime_err)
1864 }
1865
1866 #[pyo3(name = "query_order")]
1867 #[pyo3(signature = (order, client_id=None, params=None))]
1868 fn py_query_order(
1869 &mut self,
1870 py: Python<'_>,
1871 order: Py<PyAny>,
1872 client_id: Option<ClientId>,
1873 params: Option<Py<PyDict>>,
1874 ) -> PyResult<()> {
1875 let order = pyobject_to_order_any(py, order)?;
1876 let params_map = match params {
1877 Some(dict) => from_pydict(py, &dict)?,
1878 None => None,
1879 };
1880 Strategy::query_order(self.inner_mut(), &order, client_id, params_map)
1881 .map_err(to_pyruntime_err)
1882 }
1883
1884 #[pyo3(name = "on_start")]
1885 fn py_on_start(&mut self) {}
1886
1887 #[pyo3(name = "on_stop")]
1888 fn py_on_stop(&mut self) {}
1889
1890 #[pyo3(name = "on_resume")]
1891 fn py_on_resume(&mut self) {}
1892
1893 #[pyo3(name = "on_reset")]
1894 fn py_on_reset(&mut self) {}
1895
1896 #[pyo3(name = "on_dispose")]
1897 fn py_on_dispose(&mut self) {}
1898
1899 #[pyo3(name = "on_degrade")]
1900 fn py_on_degrade(&mut self) {}
1901
1902 #[pyo3(name = "on_fault")]
1903 fn py_on_fault(&mut self) {}
1904
1905 #[pyo3(name = "on_save")]
1906 fn py_on_save(&self, py: Python<'_>) -> Py<PyDict> {
1907 PyDict::new(py).unbind()
1908 }
1909
1910 #[allow(unused_variables)]
1911 #[pyo3(name = "on_load")]
1912 fn py_on_load(&mut self, state: &Bound<'_, PyDict>) {}
1913
1914 #[allow(unused_variables, clippy::needless_pass_by_value)]
1915 #[pyo3(name = "on_time_event")]
1916 fn py_on_time_event(&mut self, event: TimeEvent) {}
1917
1918 #[allow(unused_variables, clippy::needless_pass_by_value)]
1919 #[pyo3(name = "on_data")]
1920 fn py_on_data(&mut self, data: Py<PyAny>) {}
1921
1922 #[allow(unused_variables)]
1923 #[pyo3(name = "on_signal")]
1924 fn py_on_signal(&mut self, signal: &Signal) {}
1925
1926 #[allow(unused_variables, clippy::needless_pass_by_value)]
1927 #[pyo3(name = "on_instrument")]
1928 fn py_on_instrument(&mut self, instrument: Py<PyAny>) {}
1929
1930 #[allow(unused_variables)]
1931 #[pyo3(name = "on_quote")]
1932 fn py_on_quote(&mut self, quote: QuoteTick) {}
1933
1934 #[allow(unused_variables)]
1935 #[pyo3(name = "on_trade")]
1936 fn py_on_trade(&mut self, trade: TradeTick) {}
1937
1938 #[allow(unused_variables)]
1939 #[pyo3(name = "on_bar")]
1940 fn py_on_bar(&mut self, bar: Bar) {}
1941
1942 #[allow(unused_variables, clippy::needless_pass_by_value)]
1943 #[pyo3(name = "on_book_deltas")]
1944 fn py_on_book_deltas(&mut self, deltas: OrderBookDeltas) {}
1945
1946 #[allow(unused_variables)]
1947 #[pyo3(name = "on_book")]
1948 fn py_on_book(&mut self, book: &OrderBook) {}
1949
1950 #[allow(unused_variables)]
1951 #[pyo3(name = "on_mark_price")]
1952 fn py_on_mark_price(&mut self, mark_price: MarkPriceUpdate) {}
1953
1954 #[allow(unused_variables)]
1955 #[pyo3(name = "on_index_price")]
1956 fn py_on_index_price(&mut self, index_price: IndexPriceUpdate) {}
1957
1958 #[allow(unused_variables)]
1959 #[pyo3(name = "on_funding_rate")]
1960 fn py_on_funding_rate(&mut self, funding_rate: FundingRateUpdate) {}
1961
1962 #[allow(unused_variables)]
1963 #[pyo3(name = "on_instrument_status")]
1964 fn py_on_instrument_status(&mut self, status: InstrumentStatus) {}
1965
1966 #[allow(unused_variables)]
1967 #[pyo3(name = "on_instrument_close")]
1968 fn py_on_instrument_close(&mut self, close: InstrumentClose) {}
1969
1970 #[allow(unused_variables)]
1971 #[pyo3(name = "on_option_greeks")]
1972 fn py_on_option_greeks(&mut self, greeks: OptionGreeks) {}
1973
1974 #[allow(unused_variables, clippy::needless_pass_by_value)]
1975 #[pyo3(name = "on_option_chain")]
1976 fn py_on_option_chain(&mut self, slice: OptionChainSlice) {}
1977
1978 #[pyo3(name = "on_market_exit")]
1979 fn py_on_market_exit(&mut self) {}
1980
1981 #[pyo3(name = "post_market_exit")]
1982 fn py_post_market_exit(&mut self) {}
1983
1984 #[allow(unused_variables, clippy::needless_pass_by_value)]
1985 #[pyo3(name = "on_order_initialized")]
1986 fn py_on_order_initialized(&mut self, event: OrderInitialized) {}
1987
1988 #[allow(unused_variables, clippy::needless_pass_by_value)]
1989 #[pyo3(name = "on_order_event")]
1990 fn py_on_order_event(&mut self, event: Py<PyAny>) {}
1991
1992 #[allow(unused_variables)]
1993 #[pyo3(name = "on_order_denied")]
1994 fn py_on_order_denied(&mut self, event: OrderDenied) {}
1995
1996 #[allow(unused_variables)]
1997 #[pyo3(name = "on_order_emulated")]
1998 fn py_on_order_emulated(&mut self, event: OrderEmulated) {}
1999
2000 #[allow(unused_variables)]
2001 #[pyo3(name = "on_order_released")]
2002 fn py_on_order_released(&mut self, event: OrderReleased) {}
2003
2004 #[allow(unused_variables)]
2005 #[pyo3(name = "on_order_submitted")]
2006 fn py_on_order_submitted(&mut self, event: OrderSubmitted) {}
2007
2008 #[allow(unused_variables)]
2009 #[pyo3(name = "on_order_rejected")]
2010 fn py_on_order_rejected(&mut self, event: OrderRejected) {}
2011
2012 #[allow(unused_variables)]
2013 #[pyo3(name = "on_order_accepted")]
2014 fn py_on_order_accepted(&mut self, event: OrderAccepted) {}
2015
2016 #[allow(unused_variables)]
2017 #[pyo3(name = "on_order_expired")]
2018 fn py_on_order_expired(&mut self, event: OrderExpired) {}
2019
2020 #[allow(unused_variables)]
2021 #[pyo3(name = "on_order_triggered")]
2022 fn py_on_order_triggered(&mut self, event: OrderTriggered) {}
2023
2024 #[allow(unused_variables)]
2025 #[pyo3(name = "on_order_pending_update")]
2026 fn py_on_order_pending_update(&mut self, event: OrderPendingUpdate) {}
2027
2028 #[allow(unused_variables)]
2029 #[pyo3(name = "on_order_pending_cancel")]
2030 fn py_on_order_pending_cancel(&mut self, event: OrderPendingCancel) {}
2031
2032 #[allow(unused_variables)]
2033 #[pyo3(name = "on_order_modify_rejected")]
2034 fn py_on_order_modify_rejected(&mut self, event: OrderModifyRejected) {}
2035
2036 #[allow(unused_variables)]
2037 #[pyo3(name = "on_order_cancel_rejected")]
2038 fn py_on_order_cancel_rejected(&mut self, event: OrderCancelRejected) {}
2039
2040 #[allow(unused_variables)]
2041 #[pyo3(name = "on_order_updated")]
2042 fn py_on_order_updated(&mut self, event: OrderUpdated) {}
2043
2044 #[allow(unused_variables)]
2045 #[pyo3(name = "on_order_canceled")]
2046 fn py_on_order_canceled(&mut self, event: OrderCanceled) {}
2047
2048 #[allow(unused_variables)]
2049 #[pyo3(name = "on_order_filled")]
2050 fn py_on_order_filled(&mut self, event: OrderFilled) {}
2051
2052 #[allow(unused_variables, clippy::needless_pass_by_value)]
2053 #[pyo3(name = "on_position_opened")]
2054 fn py_on_position_opened(&mut self, event: PositionOpened) {}
2055
2056 #[allow(unused_variables, clippy::needless_pass_by_value)]
2057 #[pyo3(name = "on_position_event")]
2058 fn py_on_position_event(&mut self, event: Py<PyAny>) {}
2059
2060 #[allow(unused_variables, clippy::needless_pass_by_value)]
2061 #[pyo3(name = "on_position_changed")]
2062 fn py_on_position_changed(&mut self, event: PositionChanged) {}
2063
2064 #[allow(unused_variables, clippy::needless_pass_by_value)]
2065 #[pyo3(name = "on_position_closed")]
2066 fn py_on_position_closed(&mut self, event: PositionClosed) {}
2067
2068 #[allow(unused_variables, clippy::needless_pass_by_value)]
2069 #[pyo3(name = "on_historical_data")]
2070 fn py_on_historical_data(&mut self, data: Py<PyAny>) {
2071 }
2073
2074 #[allow(unused_variables, clippy::needless_pass_by_value)]
2075 #[pyo3(name = "on_historical_quotes")]
2076 fn py_on_historical_quotes(&mut self, quotes: Vec<QuoteTick>) {
2077 }
2079
2080 #[allow(unused_variables, clippy::needless_pass_by_value)]
2081 #[pyo3(name = "on_historical_trades")]
2082 fn py_on_historical_trades(&mut self, trades: Vec<TradeTick>) {
2083 }
2085
2086 #[allow(unused_variables, clippy::needless_pass_by_value)]
2087 #[pyo3(name = "on_historical_funding_rates")]
2088 fn py_on_historical_funding_rates(&mut self, funding_rates: Vec<FundingRateUpdate>) {
2089 }
2091
2092 #[allow(unused_variables, clippy::needless_pass_by_value)]
2093 #[pyo3(name = "on_historical_bars")]
2094 fn py_on_historical_bars(&mut self, bars: Vec<Bar>) {
2095 }
2097
2098 #[allow(unused_variables, clippy::needless_pass_by_value)]
2099 #[pyo3(name = "on_historical_mark_prices")]
2100 fn py_on_historical_mark_prices(&mut self, mark_prices: Vec<MarkPriceUpdate>) {
2101 }
2103
2104 #[allow(unused_variables, clippy::needless_pass_by_value)]
2105 #[pyo3(name = "on_historical_index_prices")]
2106 fn py_on_historical_index_prices(&mut self, index_prices: Vec<IndexPriceUpdate>) {
2107 }
2109
2110 #[pyo3(name = "subscribe_data")]
2111 #[pyo3(signature = (data_type, client_id=None, params=None))]
2112 fn py_subscribe_data(
2113 &mut self,
2114 data_type: DataType,
2115 client_id: Option<ClientId>,
2116 params: Option<Py<PyDict>>,
2117 ) -> PyResult<()> {
2118 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2119 match params {
2120 Some(dict) => from_pydict(py, &dict),
2121 None => Ok(None),
2122 }
2123 })?;
2124 DataActor::subscribe_data(self.inner_mut(), data_type, client_id, params_map);
2125 Ok(())
2126 }
2127
2128 #[pyo3(name = "subscribe_instruments")]
2129 #[pyo3(signature = (venue, client_id=None, params=None))]
2130 fn py_subscribe_instruments(
2131 &mut self,
2132 venue: Venue,
2133 client_id: Option<ClientId>,
2134 params: Option<Py<PyDict>>,
2135 ) -> PyResult<()> {
2136 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2137 match params {
2138 Some(dict) => from_pydict(py, &dict),
2139 None => Ok(None),
2140 }
2141 })?;
2142 DataActor::subscribe_instruments(self.inner_mut(), venue, client_id, params_map);
2143 Ok(())
2144 }
2145
2146 #[pyo3(name = "subscribe_instrument")]
2147 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2148 fn py_subscribe_instrument(
2149 &mut self,
2150 instrument_id: InstrumentId,
2151 client_id: Option<ClientId>,
2152 params: Option<Py<PyDict>>,
2153 ) -> PyResult<()> {
2154 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2155 match params {
2156 Some(dict) => from_pydict(py, &dict),
2157 None => Ok(None),
2158 }
2159 })?;
2160 DataActor::subscribe_instrument(self.inner_mut(), instrument_id, client_id, params_map);
2161 Ok(())
2162 }
2163
2164 #[pyo3(name = "subscribe_book_deltas")]
2165 #[pyo3(signature = (instrument_id, book_type, depth=None, client_id=None, managed=false, params=None))]
2166 fn py_subscribe_book_deltas(
2167 &mut self,
2168 instrument_id: InstrumentId,
2169 book_type: BookType,
2170 depth: Option<usize>,
2171 client_id: Option<ClientId>,
2172 managed: bool,
2173 params: Option<Py<PyDict>>,
2174 ) -> PyResult<()> {
2175 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2176 match params {
2177 Some(dict) => from_pydict(py, &dict),
2178 None => Ok(None),
2179 }
2180 })?;
2181 let depth = depth.and_then(NonZeroUsize::new);
2182 DataActor::subscribe_book_deltas(
2183 self.inner_mut(),
2184 instrument_id,
2185 book_type,
2186 depth,
2187 client_id,
2188 managed,
2189 params_map,
2190 );
2191 Ok(())
2192 }
2193
2194 #[pyo3(name = "subscribe_book_at_interval")]
2195 #[pyo3(signature = (instrument_id, book_type, interval_ms, depth=None, client_id=None, params=None))]
2196 fn py_subscribe_book_at_interval(
2197 &mut self,
2198 instrument_id: InstrumentId,
2199 book_type: BookType,
2200 interval_ms: usize,
2201 depth: Option<usize>,
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 let depth = depth.and_then(NonZeroUsize::new);
2212 let interval_ms = NonZeroUsize::new(interval_ms)
2213 .ok_or_else(|| to_pyvalue_err("interval_ms must be > 0"))?;
2214
2215 DataActor::subscribe_book_at_interval(
2216 self.inner_mut(),
2217 instrument_id,
2218 book_type,
2219 depth,
2220 interval_ms,
2221 client_id,
2222 params_map,
2223 );
2224 Ok(())
2225 }
2226
2227 #[pyo3(name = "subscribe_quotes")]
2228 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2229 fn py_subscribe_quotes(
2230 &mut self,
2231 instrument_id: InstrumentId,
2232 client_id: Option<ClientId>,
2233 params: Option<Py<PyDict>>,
2234 ) -> PyResult<()> {
2235 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2236 match params {
2237 Some(dict) => from_pydict(py, &dict),
2238 None => Ok(None),
2239 }
2240 })?;
2241 DataActor::subscribe_quotes(self.inner_mut(), instrument_id, client_id, params_map);
2242 Ok(())
2243 }
2244
2245 #[pyo3(name = "subscribe_trades")]
2246 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2247 fn py_subscribe_trades(
2248 &mut self,
2249 instrument_id: InstrumentId,
2250 client_id: Option<ClientId>,
2251 params: Option<Py<PyDict>>,
2252 ) -> PyResult<()> {
2253 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2254 match params {
2255 Some(dict) => from_pydict(py, &dict),
2256 None => Ok(None),
2257 }
2258 })?;
2259 DataActor::subscribe_trades(self.inner_mut(), instrument_id, client_id, params_map);
2260 Ok(())
2261 }
2262
2263 #[pyo3(name = "subscribe_bars")]
2264 #[pyo3(signature = (bar_type, client_id=None, params=None))]
2265 fn py_subscribe_bars(
2266 &mut self,
2267 bar_type: BarType,
2268 client_id: Option<ClientId>,
2269 params: Option<Py<PyDict>>,
2270 ) -> PyResult<()> {
2271 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2272 match params {
2273 Some(dict) => from_pydict(py, &dict),
2274 None => Ok(None),
2275 }
2276 })?;
2277 DataActor::subscribe_bars(self.inner_mut(), bar_type, client_id, params_map);
2278 Ok(())
2279 }
2280
2281 #[pyo3(name = "subscribe_mark_prices")]
2282 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2283 fn py_subscribe_mark_prices(
2284 &mut self,
2285 instrument_id: InstrumentId,
2286 client_id: Option<ClientId>,
2287 params: Option<Py<PyDict>>,
2288 ) -> PyResult<()> {
2289 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2290 match params {
2291 Some(dict) => from_pydict(py, &dict),
2292 None => Ok(None),
2293 }
2294 })?;
2295 DataActor::subscribe_mark_prices(self.inner_mut(), instrument_id, client_id, params_map);
2296 Ok(())
2297 }
2298
2299 #[pyo3(name = "subscribe_index_prices")]
2300 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2301 fn py_subscribe_index_prices(
2302 &mut self,
2303 instrument_id: InstrumentId,
2304 client_id: Option<ClientId>,
2305 params: Option<Py<PyDict>>,
2306 ) -> PyResult<()> {
2307 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2308 match params {
2309 Some(dict) => from_pydict(py, &dict),
2310 None => Ok(None),
2311 }
2312 })?;
2313 DataActor::subscribe_index_prices(self.inner_mut(), instrument_id, client_id, params_map);
2314 Ok(())
2315 }
2316
2317 #[pyo3(name = "subscribe_funding_rates")]
2318 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2319 fn py_subscribe_funding_rates(
2320 &mut self,
2321 instrument_id: InstrumentId,
2322 client_id: Option<ClientId>,
2323 params: Option<Py<PyDict>>,
2324 ) -> PyResult<()> {
2325 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2326 match params {
2327 Some(dict) => from_pydict(py, &dict),
2328 None => Ok(None),
2329 }
2330 })?;
2331 DataActor::subscribe_funding_rates(self.inner_mut(), instrument_id, client_id, params_map);
2332 Ok(())
2333 }
2334
2335 #[pyo3(name = "subscribe_option_greeks")]
2336 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2337 fn py_subscribe_option_greeks(
2338 &mut self,
2339 instrument_id: InstrumentId,
2340 client_id: Option<ClientId>,
2341 params: Option<Py<PyDict>>,
2342 ) -> PyResult<()> {
2343 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2344 match params {
2345 Some(dict) => from_pydict(py, &dict),
2346 None => Ok(None),
2347 }
2348 })?;
2349 DataActor::subscribe_option_greeks(self.inner_mut(), instrument_id, client_id, params_map);
2350 Ok(())
2351 }
2352
2353 #[pyo3(name = "subscribe_instrument_status")]
2354 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2355 fn py_subscribe_instrument_status(
2356 &mut self,
2357 instrument_id: InstrumentId,
2358 client_id: Option<ClientId>,
2359 params: Option<Py<PyDict>>,
2360 ) -> PyResult<()> {
2361 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2362 match params {
2363 Some(dict) => from_pydict(py, &dict),
2364 None => Ok(None),
2365 }
2366 })?;
2367 DataActor::subscribe_instrument_status(
2368 self.inner_mut(),
2369 instrument_id,
2370 client_id,
2371 params_map,
2372 );
2373 Ok(())
2374 }
2375
2376 #[pyo3(name = "subscribe_instrument_close")]
2377 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2378 fn py_subscribe_instrument_close(
2379 &mut self,
2380 instrument_id: InstrumentId,
2381 client_id: Option<ClientId>,
2382 params: Option<Py<PyDict>>,
2383 ) -> PyResult<()> {
2384 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2385 match params {
2386 Some(dict) => from_pydict(py, &dict),
2387 None => Ok(None),
2388 }
2389 })?;
2390 DataActor::subscribe_instrument_close(
2391 self.inner_mut(),
2392 instrument_id,
2393 client_id,
2394 params_map,
2395 );
2396 Ok(())
2397 }
2398
2399 #[pyo3(name = "subscribe_option_chain")]
2400 #[pyo3(signature = (series_id, strike_range, snapshot_interval_ms=None, client_id=None, params=None))]
2401 fn py_subscribe_option_chain(
2402 &mut self,
2403 py: Python<'_>,
2404 series_id: OptionSeriesId,
2405 strike_range: PyStrikeRange,
2406 snapshot_interval_ms: Option<u64>,
2407 client_id: Option<ClientId>,
2408 params: Option<Py<PyDict>>,
2409 ) -> PyResult<()> {
2410 let params_map = match params {
2411 Some(dict) => from_pydict(py, &dict)?,
2412 None => None,
2413 };
2414 DataActor::subscribe_option_chain(
2415 self.inner_mut(),
2416 series_id,
2417 strike_range.inner,
2418 snapshot_interval_ms,
2419 client_id,
2420 params_map,
2421 );
2422 Ok(())
2423 }
2424
2425 #[pyo3(name = "subscribe_order_fills")]
2426 #[pyo3(signature = (instrument_id))]
2427 fn py_subscribe_order_fills(&mut self, instrument_id: InstrumentId) {
2428 DataActor::subscribe_order_fills(self.inner_mut(), instrument_id);
2429 }
2430
2431 #[pyo3(name = "subscribe_order_cancels")]
2432 #[pyo3(signature = (instrument_id))]
2433 fn py_subscribe_order_cancels(&mut self, instrument_id: InstrumentId) {
2434 DataActor::subscribe_order_cancels(self.inner_mut(), instrument_id);
2435 }
2436
2437 #[pyo3(name = "unsubscribe_data")]
2438 #[pyo3(signature = (data_type, client_id=None, params=None))]
2439 fn py_unsubscribe_data(
2440 &mut self,
2441 data_type: DataType,
2442 client_id: Option<ClientId>,
2443 params: Option<Py<PyDict>>,
2444 ) -> PyResult<()> {
2445 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2446 match params {
2447 Some(dict) => from_pydict(py, &dict),
2448 None => Ok(None),
2449 }
2450 })?;
2451 DataActor::unsubscribe_data(self.inner_mut(), data_type, client_id, params_map);
2452 Ok(())
2453 }
2454
2455 #[pyo3(name = "unsubscribe_instruments")]
2456 #[pyo3(signature = (venue, client_id=None, params=None))]
2457 fn py_unsubscribe_instruments(
2458 &mut self,
2459 venue: Venue,
2460 client_id: Option<ClientId>,
2461 params: Option<Py<PyDict>>,
2462 ) -> PyResult<()> {
2463 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2464 match params {
2465 Some(dict) => from_pydict(py, &dict),
2466 None => Ok(None),
2467 }
2468 })?;
2469 DataActor::unsubscribe_instruments(self.inner_mut(), venue, client_id, params_map);
2470 Ok(())
2471 }
2472
2473 #[pyo3(name = "unsubscribe_instrument")]
2474 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2475 fn py_unsubscribe_instrument(
2476 &mut self,
2477 instrument_id: InstrumentId,
2478 client_id: Option<ClientId>,
2479 params: Option<Py<PyDict>>,
2480 ) -> PyResult<()> {
2481 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2482 match params {
2483 Some(dict) => from_pydict(py, &dict),
2484 None => Ok(None),
2485 }
2486 })?;
2487 DataActor::unsubscribe_instrument(self.inner_mut(), instrument_id, client_id, params_map);
2488 Ok(())
2489 }
2490
2491 #[pyo3(name = "unsubscribe_book_deltas")]
2492 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2493 fn py_unsubscribe_book_deltas(
2494 &mut self,
2495 instrument_id: InstrumentId,
2496 client_id: Option<ClientId>,
2497 params: Option<Py<PyDict>>,
2498 ) -> PyResult<()> {
2499 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2500 match params {
2501 Some(dict) => from_pydict(py, &dict),
2502 None => Ok(None),
2503 }
2504 })?;
2505 DataActor::unsubscribe_book_deltas(self.inner_mut(), instrument_id, client_id, params_map);
2506 Ok(())
2507 }
2508
2509 #[pyo3(name = "unsubscribe_book_at_interval")]
2510 #[pyo3(signature = (instrument_id, interval_ms, client_id=None, params=None))]
2511 fn py_unsubscribe_book_at_interval(
2512 &mut self,
2513 instrument_id: InstrumentId,
2514 interval_ms: usize,
2515 client_id: Option<ClientId>,
2516 params: Option<Py<PyDict>>,
2517 ) -> PyResult<()> {
2518 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2519 match params {
2520 Some(dict) => from_pydict(py, &dict),
2521 None => Ok(None),
2522 }
2523 })?;
2524 let interval_ms = NonZeroUsize::new(interval_ms)
2525 .ok_or_else(|| to_pyvalue_err("interval_ms must be > 0"))?;
2526
2527 DataActor::unsubscribe_book_at_interval(
2528 self.inner_mut(),
2529 instrument_id,
2530 interval_ms,
2531 client_id,
2532 params_map,
2533 );
2534 Ok(())
2535 }
2536
2537 #[pyo3(name = "unsubscribe_quotes")]
2538 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2539 fn py_unsubscribe_quotes(
2540 &mut self,
2541 instrument_id: InstrumentId,
2542 client_id: Option<ClientId>,
2543 params: Option<Py<PyDict>>,
2544 ) -> PyResult<()> {
2545 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2546 match params {
2547 Some(dict) => from_pydict(py, &dict),
2548 None => Ok(None),
2549 }
2550 })?;
2551 DataActor::unsubscribe_quotes(self.inner_mut(), instrument_id, client_id, params_map);
2552 Ok(())
2553 }
2554
2555 #[pyo3(name = "unsubscribe_trades")]
2556 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2557 fn py_unsubscribe_trades(
2558 &mut self,
2559 instrument_id: InstrumentId,
2560 client_id: Option<ClientId>,
2561 params: Option<Py<PyDict>>,
2562 ) -> PyResult<()> {
2563 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2564 match params {
2565 Some(dict) => from_pydict(py, &dict),
2566 None => Ok(None),
2567 }
2568 })?;
2569 DataActor::unsubscribe_trades(self.inner_mut(), instrument_id, client_id, params_map);
2570 Ok(())
2571 }
2572
2573 #[pyo3(name = "unsubscribe_bars")]
2574 #[pyo3(signature = (bar_type, client_id=None, params=None))]
2575 fn py_unsubscribe_bars(
2576 &mut self,
2577 bar_type: BarType,
2578 client_id: Option<ClientId>,
2579 params: Option<Py<PyDict>>,
2580 ) -> PyResult<()> {
2581 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2582 match params {
2583 Some(dict) => from_pydict(py, &dict),
2584 None => Ok(None),
2585 }
2586 })?;
2587 DataActor::unsubscribe_bars(self.inner_mut(), bar_type, client_id, params_map);
2588 Ok(())
2589 }
2590
2591 #[pyo3(name = "unsubscribe_mark_prices")]
2592 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2593 fn py_unsubscribe_mark_prices(
2594 &mut self,
2595 instrument_id: InstrumentId,
2596 client_id: Option<ClientId>,
2597 params: Option<Py<PyDict>>,
2598 ) -> PyResult<()> {
2599 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2600 match params {
2601 Some(dict) => from_pydict(py, &dict),
2602 None => Ok(None),
2603 }
2604 })?;
2605 DataActor::unsubscribe_mark_prices(self.inner_mut(), instrument_id, client_id, params_map);
2606 Ok(())
2607 }
2608
2609 #[pyo3(name = "unsubscribe_index_prices")]
2610 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2611 fn py_unsubscribe_index_prices(
2612 &mut self,
2613 instrument_id: InstrumentId,
2614 client_id: Option<ClientId>,
2615 params: Option<Py<PyDict>>,
2616 ) -> PyResult<()> {
2617 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2618 match params {
2619 Some(dict) => from_pydict(py, &dict),
2620 None => Ok(None),
2621 }
2622 })?;
2623 DataActor::unsubscribe_index_prices(self.inner_mut(), instrument_id, client_id, params_map);
2624 Ok(())
2625 }
2626
2627 #[pyo3(name = "unsubscribe_funding_rates")]
2628 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2629 fn py_unsubscribe_funding_rates(
2630 &mut self,
2631 instrument_id: InstrumentId,
2632 client_id: Option<ClientId>,
2633 params: Option<Py<PyDict>>,
2634 ) -> PyResult<()> {
2635 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2636 match params {
2637 Some(dict) => from_pydict(py, &dict),
2638 None => Ok(None),
2639 }
2640 })?;
2641 DataActor::unsubscribe_funding_rates(
2642 self.inner_mut(),
2643 instrument_id,
2644 client_id,
2645 params_map,
2646 );
2647 Ok(())
2648 }
2649
2650 #[pyo3(name = "unsubscribe_option_greeks")]
2651 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2652 fn py_unsubscribe_option_greeks(
2653 &mut self,
2654 instrument_id: InstrumentId,
2655 client_id: Option<ClientId>,
2656 params: Option<Py<PyDict>>,
2657 ) -> PyResult<()> {
2658 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2659 match params {
2660 Some(dict) => from_pydict(py, &dict),
2661 None => Ok(None),
2662 }
2663 })?;
2664 DataActor::unsubscribe_option_greeks(
2665 self.inner_mut(),
2666 instrument_id,
2667 client_id,
2668 params_map,
2669 );
2670 Ok(())
2671 }
2672
2673 #[pyo3(name = "unsubscribe_instrument_status")]
2674 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2675 fn py_unsubscribe_instrument_status(
2676 &mut self,
2677 instrument_id: InstrumentId,
2678 client_id: Option<ClientId>,
2679 params: Option<Py<PyDict>>,
2680 ) -> PyResult<()> {
2681 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2682 match params {
2683 Some(dict) => from_pydict(py, &dict),
2684 None => Ok(None),
2685 }
2686 })?;
2687 DataActor::unsubscribe_instrument_status(
2688 self.inner_mut(),
2689 instrument_id,
2690 client_id,
2691 params_map,
2692 );
2693 Ok(())
2694 }
2695
2696 #[pyo3(name = "unsubscribe_instrument_close")]
2697 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2698 fn py_unsubscribe_instrument_close(
2699 &mut self,
2700 instrument_id: InstrumentId,
2701 client_id: Option<ClientId>,
2702 params: Option<Py<PyDict>>,
2703 ) -> PyResult<()> {
2704 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2705 match params {
2706 Some(dict) => from_pydict(py, &dict),
2707 None => Ok(None),
2708 }
2709 })?;
2710 DataActor::unsubscribe_instrument_close(
2711 self.inner_mut(),
2712 instrument_id,
2713 client_id,
2714 params_map,
2715 );
2716 Ok(())
2717 }
2718
2719 #[pyo3(name = "unsubscribe_option_chain")]
2720 #[pyo3(signature = (series_id, client_id=None))]
2721 fn py_unsubscribe_option_chain(
2722 &mut self,
2723 series_id: OptionSeriesId,
2724 client_id: Option<ClientId>,
2725 ) {
2726 DataActor::unsubscribe_option_chain(self.inner_mut(), series_id, client_id);
2727 }
2728
2729 #[pyo3(name = "unsubscribe_order_fills")]
2730 #[pyo3(signature = (instrument_id))]
2731 fn py_unsubscribe_order_fills(&mut self, instrument_id: InstrumentId) {
2732 DataActor::unsubscribe_order_fills(self.inner_mut(), instrument_id);
2733 }
2734
2735 #[pyo3(name = "unsubscribe_order_cancels")]
2736 #[pyo3(signature = (instrument_id))]
2737 fn py_unsubscribe_order_cancels(&mut self, instrument_id: InstrumentId) {
2738 DataActor::unsubscribe_order_cancels(self.inner_mut(), instrument_id);
2739 }
2740
2741 #[pyo3(name = "request_data")]
2742 #[pyo3(signature = (data_type, client_id, start=None, end=None, limit=None, params=None))]
2743 fn py_request_data(
2744 &mut self,
2745 data_type: DataType,
2746 client_id: ClientId,
2747 start: Option<DateTime<Utc>>,
2748 end: Option<DateTime<Utc>>,
2749 limit: Option<usize>,
2750 params: Option<Py<PyDict>>,
2751 ) -> PyResult<String> {
2752 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2753 match params {
2754 Some(dict) => from_pydict(py, &dict),
2755 None => Ok(None),
2756 }
2757 })?;
2758 let limit = limit.and_then(NonZeroUsize::new);
2759 let request_id = DataActor::request_data(
2760 self.inner_mut(),
2761 data_type,
2762 client_id,
2763 start,
2764 end,
2765 limit,
2766 params_map,
2767 )
2768 .map_err(to_pyvalue_err)?;
2769 Ok(request_id.to_string())
2770 }
2771
2772 #[pyo3(name = "request_instrument")]
2773 #[pyo3(signature = (instrument_id, start=None, end=None, client_id=None, params=None))]
2774 fn py_request_instrument(
2775 &mut self,
2776 instrument_id: InstrumentId,
2777 start: Option<DateTime<Utc>>,
2778 end: Option<DateTime<Utc>>,
2779 client_id: Option<ClientId>,
2780 params: Option<Py<PyDict>>,
2781 ) -> PyResult<String> {
2782 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2783 match params {
2784 Some(dict) => from_pydict(py, &dict),
2785 None => Ok(None),
2786 }
2787 })?;
2788 let request_id = DataActor::request_instrument(
2789 self.inner_mut(),
2790 instrument_id,
2791 start,
2792 end,
2793 client_id,
2794 params_map,
2795 )
2796 .map_err(to_pyvalue_err)?;
2797 Ok(request_id.to_string())
2798 }
2799
2800 #[pyo3(name = "request_instruments")]
2801 #[pyo3(signature = (venue=None, start=None, end=None, client_id=None, params=None))]
2802 fn py_request_instruments(
2803 &mut self,
2804 venue: Option<Venue>,
2805 start: Option<DateTime<Utc>>,
2806 end: Option<DateTime<Utc>>,
2807 client_id: Option<ClientId>,
2808 params: Option<Py<PyDict>>,
2809 ) -> PyResult<String> {
2810 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2811 match params {
2812 Some(dict) => from_pydict(py, &dict),
2813 None => Ok(None),
2814 }
2815 })?;
2816 let request_id = DataActor::request_instruments(
2817 self.inner_mut(),
2818 venue,
2819 start,
2820 end,
2821 client_id,
2822 params_map,
2823 )
2824 .map_err(to_pyvalue_err)?;
2825 Ok(request_id.to_string())
2826 }
2827
2828 #[pyo3(name = "request_book_snapshot")]
2829 #[pyo3(signature = (instrument_id, depth=None, client_id=None, params=None))]
2830 fn py_request_book_snapshot(
2831 &mut self,
2832 instrument_id: InstrumentId,
2833 depth: Option<usize>,
2834 client_id: Option<ClientId>,
2835 params: Option<Py<PyDict>>,
2836 ) -> PyResult<String> {
2837 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2838 match params {
2839 Some(dict) => from_pydict(py, &dict),
2840 None => Ok(None),
2841 }
2842 })?;
2843 let depth = depth.and_then(NonZeroUsize::new);
2844
2845 let request_id = DataActor::request_book_snapshot(
2846 self.inner_mut(),
2847 instrument_id,
2848 depth,
2849 client_id,
2850 params_map,
2851 )
2852 .map_err(to_pyvalue_err)?;
2853 Ok(request_id.to_string())
2854 }
2855
2856 #[pyo3(name = "request_book_deltas")]
2857 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None))]
2858 fn py_request_book_deltas(
2859 &mut self,
2860 instrument_id: InstrumentId,
2861 start: Option<DateTime<Utc>>,
2862 end: Option<DateTime<Utc>>,
2863 limit: Option<usize>,
2864 client_id: Option<ClientId>,
2865 params: Option<Py<PyDict>>,
2866 ) -> PyResult<String> {
2867 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2868 match params {
2869 Some(dict) => from_pydict(py, &dict),
2870 None => Ok(None),
2871 }
2872 })?;
2873 let limit = limit.and_then(NonZeroUsize::new);
2874 let request_id = DataActor::request_book_deltas(
2875 self.inner_mut(),
2876 instrument_id,
2877 start,
2878 end,
2879 limit,
2880 client_id,
2881 params_map,
2882 )
2883 .map_err(to_pyvalue_err)?;
2884 Ok(request_id.to_string())
2885 }
2886
2887 #[pyo3(name = "request_book_depth")]
2888 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, depth=None, client_id=None, params=None))]
2889 #[expect(clippy::too_many_arguments)]
2890 fn py_request_book_depth(
2891 &mut self,
2892 instrument_id: InstrumentId,
2893 start: Option<DateTime<Utc>>,
2894 end: Option<DateTime<Utc>>,
2895 limit: Option<usize>,
2896 depth: Option<usize>,
2897 client_id: Option<ClientId>,
2898 params: Option<Py<PyDict>>,
2899 ) -> PyResult<String> {
2900 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2901 match params {
2902 Some(dict) => from_pydict(py, &dict),
2903 None => Ok(None),
2904 }
2905 })?;
2906 let limit = limit.and_then(NonZeroUsize::new);
2907 let depth = depth.and_then(NonZeroUsize::new);
2908 let request_id = DataActor::request_book_depth(
2909 self.inner_mut(),
2910 instrument_id,
2911 start,
2912 end,
2913 limit,
2914 depth,
2915 client_id,
2916 params_map,
2917 )
2918 .map_err(to_pyvalue_err)?;
2919 Ok(request_id.to_string())
2920 }
2921
2922 #[pyo3(name = "request_quotes")]
2923 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None))]
2924 fn py_request_quotes(
2925 &mut self,
2926 instrument_id: InstrumentId,
2927 start: Option<DateTime<Utc>>,
2928 end: Option<DateTime<Utc>>,
2929 limit: Option<usize>,
2930 client_id: Option<ClientId>,
2931 params: Option<Py<PyDict>>,
2932 ) -> PyResult<String> {
2933 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2934 match params {
2935 Some(dict) => from_pydict(py, &dict),
2936 None => Ok(None),
2937 }
2938 })?;
2939 let limit = limit.and_then(NonZeroUsize::new);
2940 let request_id = DataActor::request_quotes(
2941 self.inner_mut(),
2942 instrument_id,
2943 start,
2944 end,
2945 limit,
2946 client_id,
2947 params_map,
2948 )
2949 .map_err(to_pyvalue_err)?;
2950 Ok(request_id.to_string())
2951 }
2952
2953 #[pyo3(name = "request_trades")]
2954 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None))]
2955 fn py_request_trades(
2956 &mut self,
2957 instrument_id: InstrumentId,
2958 start: Option<DateTime<Utc>>,
2959 end: Option<DateTime<Utc>>,
2960 limit: Option<usize>,
2961 client_id: Option<ClientId>,
2962 params: Option<Py<PyDict>>,
2963 ) -> PyResult<String> {
2964 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2965 match params {
2966 Some(dict) => from_pydict(py, &dict),
2967 None => Ok(None),
2968 }
2969 })?;
2970 let limit = limit.and_then(NonZeroUsize::new);
2971 let request_id = DataActor::request_trades(
2972 self.inner_mut(),
2973 instrument_id,
2974 start,
2975 end,
2976 limit,
2977 client_id,
2978 params_map,
2979 )
2980 .map_err(to_pyvalue_err)?;
2981 Ok(request_id.to_string())
2982 }
2983
2984 #[pyo3(name = "request_funding_rates")]
2985 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None))]
2986 fn py_request_funding_rates(
2987 &mut self,
2988 instrument_id: InstrumentId,
2989 start: Option<DateTime<Utc>>,
2990 end: Option<DateTime<Utc>>,
2991 limit: Option<usize>,
2992 client_id: Option<ClientId>,
2993 params: Option<Py<PyDict>>,
2994 ) -> PyResult<String> {
2995 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
2996 match params {
2997 Some(dict) => from_pydict(py, &dict),
2998 None => Ok(None),
2999 }
3000 })?;
3001 let limit = limit.and_then(NonZeroUsize::new);
3002 let request_id = DataActor::request_funding_rates(
3003 self.inner_mut(),
3004 instrument_id,
3005 start,
3006 end,
3007 limit,
3008 client_id,
3009 params_map,
3010 )
3011 .map_err(to_pyvalue_err)?;
3012 Ok(request_id.to_string())
3013 }
3014
3015 #[pyo3(name = "request_bars")]
3016 #[pyo3(signature = (bar_type, start=None, end=None, limit=None, client_id=None, params=None))]
3017 fn py_request_bars(
3018 &mut self,
3019 bar_type: BarType,
3020 start: Option<DateTime<Utc>>,
3021 end: Option<DateTime<Utc>>,
3022 limit: Option<usize>,
3023 client_id: Option<ClientId>,
3024 params: Option<Py<PyDict>>,
3025 ) -> PyResult<String> {
3026 let params_map = Python::attach(|py| -> PyResult<Option<Params>> {
3027 match params {
3028 Some(dict) => from_pydict(py, &dict),
3029 None => Ok(None),
3030 }
3031 })?;
3032 let limit = limit.and_then(NonZeroUsize::new);
3033 let request_id = DataActor::request_bars(
3034 self.inner_mut(),
3035 bar_type,
3036 start,
3037 end,
3038 limit,
3039 client_id,
3040 params_map,
3041 )
3042 .map_err(to_pyvalue_err)?;
3043 Ok(request_id.to_string())
3044 }
3045}
3046
3047fn py_order_list_to_orders(py: Python<'_>, order_list: &Py<PyAny>) -> PyResult<Vec<OrderAny>> {
3048 let order_objects = match order_list.getattr(py, "orders") {
3049 Ok(orders) => orders.extract::<Vec<Py<PyAny>>>(py)?,
3050 Err(e) if e.is_instance_of::<pyo3::exceptions::PyAttributeError>(py) => {
3051 order_list.extract::<Vec<Py<PyAny>>>(py)?
3052 }
3053 Err(e) => return Err(e),
3054 };
3055
3056 order_objects
3057 .into_iter()
3058 .map(|order| pyobject_to_order_any(py, order))
3059 .collect()
3060}
3061
3062#[cfg(test)]
3063mod tests {
3064 use std::{
3065 cell::RefCell,
3066 collections::{BTreeMap, HashMap},
3067 rc::Rc,
3068 str::FromStr,
3069 };
3070
3071 use indexmap::IndexMap;
3072 use nautilus_common::{
3073 actor::DataActor,
3074 cache::Cache,
3075 clock::{Clock, TestClock},
3076 component::Component,
3077 messages::{
3078 data::{BarsResponse, QuotesResponse, TradesResponse},
3079 execution::TradingCommand,
3080 },
3081 msgbus::{
3082 self, MessagingSwitchboard,
3083 stubs::{TypedIntoMessageSavingHandler, get_typed_into_message_saving_handler},
3084 },
3085 signal::Signal,
3086 timer::TimeEvent,
3087 };
3088 use nautilus_core::{UUID4, UnixNanos};
3089 use nautilus_model::{
3090 data::{
3091 Bar, BarType, CustomData, FundingRateUpdate, IndexPriceUpdate, InstrumentStatus,
3092 MarkPriceUpdate, OrderBookDelta, OrderBookDeltas, QuoteTick, TradeTick,
3093 close::InstrumentClose,
3094 greeks::OptionGreekValues,
3095 option_chain::{OptionChainSlice, OptionGreeks},
3096 stubs::stub_custom_data,
3097 },
3098 enums::{
3099 AggressorSide, BookType, GreeksConvention, InstrumentCloseType, MarketStatusAction,
3100 OrderSide, OrderType, PositionSide, TimeInForce,
3101 },
3102 events::{
3103 OrderAccepted, OrderCancelRejected, OrderCanceled, OrderDenied, OrderEmulated,
3104 OrderEventAny, OrderExpired, OrderInitialized, OrderModifyRejected, OrderPendingCancel,
3105 OrderPendingUpdate, OrderRejected, OrderReleased, OrderSubmitted, OrderTriggered,
3106 OrderUpdated, PositionChanged, PositionClosed, PositionEvent, PositionOpened,
3107 order::spec::OrderFilledSpec,
3108 },
3109 identifiers::{
3110 AccountId, ClientId, ClientOrderId, InstrumentId, OptionSeriesId, PositionId,
3111 StrategyId, TradeId, TraderId, Venue,
3112 },
3113 instruments::{CurrencyPair, InstrumentAny, stubs::audusd_sim},
3114 orderbook::OrderBook,
3115 orders::{Order, OrderTestBuilder},
3116 python::orders::order_any_to_pyobject,
3117 types::{Currency, Money, Price, Quantity},
3118 };
3119 use nautilus_portfolio::portfolio::Portfolio;
3120 use pyo3::{
3121 Bound, Py, PyAny, PyResult, Python,
3122 ffi::c_str,
3123 types::{PyAnyMethods, PyBytes, PyDict, PyList},
3124 };
3125 use serde_json::Value;
3126 use ustr::Ustr;
3127
3128 use super::PyStrategy;
3129 use crate::strategy::{Strategy, StrategyConfig};
3130
3131 const TRACKING_STRATEGY_CODE: &std::ffi::CStr = c_str!(
3132 r#"
3133class TrackingStrategy:
3134 TRACKED_METHODS = {
3135 "on_start",
3136 "on_stop",
3137 "on_resume",
3138 "on_reset",
3139 "on_dispose",
3140 "on_degrade",
3141 "on_fault",
3142 "on_save",
3143 "on_load",
3144 "on_time_event",
3145 "on_data",
3146 "on_signal",
3147 "on_instrument",
3148 "on_quote",
3149 "on_trade",
3150 "on_bar",
3151 "on_book_deltas",
3152 "on_book",
3153 "on_mark_price",
3154 "on_index_price",
3155 "on_funding_rate",
3156 "on_instrument_status",
3157 "on_instrument_close",
3158 "on_option_greeks",
3159 "on_option_chain",
3160 "on_historical_data",
3161 "on_historical_quotes",
3162 "on_historical_trades",
3163 "on_historical_funding_rates",
3164 "on_historical_bars",
3165 "on_historical_mark_prices",
3166 "on_historical_index_prices",
3167 "on_market_exit",
3168 "post_market_exit",
3169 "on_order_initialized",
3170 "on_order_event",
3171 "on_order_denied",
3172 "on_order_emulated",
3173 "on_order_released",
3174 "on_order_submitted",
3175 "on_order_rejected",
3176 "on_order_accepted",
3177 "on_order_expired",
3178 "on_order_triggered",
3179 "on_order_pending_update",
3180 "on_order_pending_cancel",
3181 "on_order_modify_rejected",
3182 "on_order_cancel_rejected",
3183 "on_order_updated",
3184 "on_order_canceled",
3185 "on_order_filled",
3186 "on_position_opened",
3187 "on_position_event",
3188 "on_position_changed",
3189 "on_position_closed",
3190 }
3191
3192 def __init__(self):
3193 self.calls = []
3194
3195 def _record(self, method_name, *args):
3196 self.calls.append((method_name, args))
3197
3198 def was_called(self, method_name):
3199 return any(call[0] == method_name for call in self.calls)
3200
3201 def call_count(self, method_name):
3202 return sum(1 for call in self.calls if call[0] == method_name)
3203
3204 def call_names(self):
3205 return [call[0] for call in self.calls]
3206
3207 def last_loaded_state(self):
3208
3209 for method_name, args in reversed(self.calls):
3210 if method_name == "on_load":
3211 return args[0]
3212 return None
3213
3214 def on_save(self):
3215 self._record("on_save")
3216 return {"strategy": b"saved"}
3217
3218 def on_load(self, state):
3219 self._record("on_load", dict(state))
3220
3221 def __getattr__(self, name):
3222 if name in self.TRACKED_METHODS:
3223 return lambda *args: self._record(name, *args)
3224 raise AttributeError(name)
3225"#
3226 );
3227
3228 fn create_tracking_python_strategy(py: Python<'_>) -> PyResult<Py<PyAny>> {
3229 py.run(TRACKING_STRATEGY_CODE, None, None)?;
3230 let tracking_strategy_class = py.eval(c_str!("TrackingStrategy"), None, None)?;
3231 let instance = tracking_strategy_class.call0()?;
3232 Ok(instance.unbind())
3233 }
3234
3235 fn python_method_was_called(
3236 py_strategy: &Py<PyAny>,
3237 py: Python<'_>,
3238 method_name: &str,
3239 ) -> bool {
3240 py_strategy
3241 .call_method1(py, "was_called", (method_name,))
3242 .and_then(|result| result.extract::<bool>(py))
3243 .unwrap_or(false)
3244 }
3245
3246 fn python_method_call_count(py_strategy: &Py<PyAny>, py: Python<'_>, method_name: &str) -> i32 {
3247 py_strategy
3248 .call_method1(py, "call_count", (method_name,))
3249 .and_then(|result| result.extract::<i32>(py))
3250 .unwrap_or(0)
3251 }
3252
3253 fn python_method_call_names(py_strategy: &Py<PyAny>, py: Python<'_>) -> Vec<String> {
3254 py_strategy
3255 .call_method0(py, "call_names")
3256 .and_then(|result| result.extract::<Vec<String>>(py))
3257 .unwrap_or_default()
3258 }
3259
3260 fn python_last_loaded_state(
3261 py_strategy: &Py<PyAny>,
3262 py: Python<'_>,
3263 ) -> Option<HashMap<String, Vec<u8>>> {
3264 py_strategy
3265 .call_method0(py, "last_loaded_state")
3266 .and_then(|result| result.extract::<Option<HashMap<String, Vec<u8>>>>(py))
3267 .unwrap_or(None)
3268 }
3269
3270 const TRACKING_INDICATOR_CODE: &std::ffi::CStr = c_str!(
3271 r#"
3272class TrackingIndicator:
3273 def __init__(self, events=None):
3274 self.initialized = False
3275 self.calls = []
3276 self.events = events
3277
3278 def handle_quote_tick(self, quote):
3279 self.calls.append("quote")
3280 if self.events is not None:
3281 self.events.append("indicator:quote")
3282
3283 def handle_trade_tick(self, trade):
3284 self.calls.append("trade")
3285 if self.events is not None:
3286 self.events.append("indicator:trade")
3287
3288 def handle_bar(self, bar):
3289 self.calls.append("bar")
3290 if self.events is not None:
3291 self.events.append("indicator:bar")
3292
3293 def call_count(self, name):
3294 return self.calls.count(name)
3295
3296class IndicatorEventStrategy:
3297 def __init__(self, events):
3298 self.events = events
3299
3300 def on_start(self):
3301 pass
3302
3303 def on_quote(self, quote):
3304 self.events.append("strategy:quote")
3305
3306 def on_trade(self, trade):
3307 self.events.append("strategy:trade")
3308
3309 def on_bar(self, bar):
3310 self.events.append("strategy:bar")
3311"#
3312 );
3313
3314 fn create_tracking_python_indicator(py: Python<'_>) -> PyResult<Py<PyAny>> {
3315 py.run(TRACKING_INDICATOR_CODE, None, None)?;
3316 let indicator_class = py.eval(c_str!("TrackingIndicator"), None, None)?;
3317 Ok(indicator_class.call0()?.unbind())
3318 }
3319
3320 fn create_event_tracking_python_indicator(
3321 py: Python<'_>,
3322 events: &Bound<'_, PyList>,
3323 ) -> PyResult<Py<PyAny>> {
3324 py.run(TRACKING_INDICATOR_CODE, None, None)?;
3325 let indicator_class = py.eval(c_str!("TrackingIndicator"), None, None)?;
3326 Ok(indicator_class.call1((events,))?.unbind())
3327 }
3328
3329 fn create_indicator_event_strategy(
3330 py: Python<'_>,
3331 events: &Bound<'_, PyList>,
3332 ) -> PyResult<Py<PyAny>> {
3333 py.run(TRACKING_INDICATOR_CODE, None, None)?;
3334 let strategy_class = py.eval(c_str!("IndicatorEventStrategy"), None, None)?;
3335 Ok(strategy_class.call1((events,))?.unbind())
3336 }
3337
3338 fn python_indicator_call_count(
3339 indicator: &Py<PyAny>,
3340 py: Python<'_>,
3341 method_name: &str,
3342 ) -> i32 {
3343 indicator
3344 .call_method1(py, "call_count", (method_name,))
3345 .and_then(|result| result.extract::<i32>(py))
3346 .unwrap_or(0)
3347 }
3348
3349 fn sample_instrument() -> CurrencyPair {
3350 audusd_sim()
3351 }
3352
3353 fn sample_time_event() -> TimeEvent {
3354 TimeEvent::new(
3355 Ustr::from("test_timer"),
3356 UUID4::new(),
3357 UnixNanos::default(),
3358 UnixNanos::default(),
3359 )
3360 }
3361
3362 fn sample_data() -> CustomData {
3363 stub_custom_data(1, 42, None, None)
3364 }
3365
3366 fn sample_signal() -> Signal {
3367 Signal::new(
3368 Ustr::from("test_signal"),
3369 "1.0".to_string(),
3370 UnixNanos::default(),
3371 UnixNanos::default(),
3372 )
3373 }
3374
3375 fn sample_quote() -> QuoteTick {
3376 let instrument = sample_instrument();
3377 QuoteTick::new(
3378 instrument.id,
3379 Price::from("1.00000"),
3380 Price::from("1.00001"),
3381 Quantity::from(100_000),
3382 Quantity::from(100_000),
3383 UnixNanos::default(),
3384 UnixNanos::default(),
3385 )
3386 }
3387
3388 fn sample_trade() -> TradeTick {
3389 let instrument = sample_instrument();
3390 TradeTick::new(
3391 instrument.id,
3392 Price::from("1.00000"),
3393 Quantity::from(100_000),
3394 AggressorSide::Buyer,
3395 TradeId::new("123456"),
3396 UnixNanos::default(),
3397 UnixNanos::default(),
3398 )
3399 }
3400
3401 fn sample_bar() -> Bar {
3402 let instrument = sample_instrument();
3403 let bar_type =
3404 BarType::from_str(&format!("{}-1-MINUTE-LAST-INTERNAL", instrument.id)).unwrap();
3405 Bar::new(
3406 bar_type,
3407 Price::from("1.00000"),
3408 Price::from("1.00010"),
3409 Price::from("0.99990"),
3410 Price::from("1.00005"),
3411 Quantity::from(100_000),
3412 UnixNanos::default(),
3413 UnixNanos::default(),
3414 )
3415 }
3416
3417 fn sample_book() -> OrderBook {
3418 OrderBook::new(sample_instrument().id, BookType::L2_MBP)
3419 }
3420
3421 fn sample_book_deltas() -> OrderBookDeltas {
3422 let instrument = sample_instrument();
3423 let delta =
3424 OrderBookDelta::clear(instrument.id, 0, UnixNanos::default(), UnixNanos::default());
3425 OrderBookDeltas::new(instrument.id, vec![delta])
3426 }
3427
3428 fn sample_mark_price() -> MarkPriceUpdate {
3429 MarkPriceUpdate::new(
3430 sample_instrument().id,
3431 Price::from("1.00000"),
3432 UnixNanos::default(),
3433 UnixNanos::default(),
3434 )
3435 }
3436
3437 fn sample_index_price() -> IndexPriceUpdate {
3438 IndexPriceUpdate::new(
3439 sample_instrument().id,
3440 Price::from("1.00000"),
3441 UnixNanos::default(),
3442 UnixNanos::default(),
3443 )
3444 }
3445
3446 fn sample_funding_rate() -> FundingRateUpdate {
3447 FundingRateUpdate::new(
3448 sample_instrument().id,
3449 "0.0001".parse().unwrap(),
3450 None,
3451 None,
3452 UnixNanos::default(),
3453 UnixNanos::default(),
3454 )
3455 }
3456
3457 fn sample_instrument_status() -> InstrumentStatus {
3458 InstrumentStatus::new(
3459 sample_instrument().id,
3460 MarketStatusAction::Trading,
3461 UnixNanos::default(),
3462 UnixNanos::default(),
3463 None,
3464 None,
3465 None,
3466 None,
3467 None,
3468 )
3469 }
3470
3471 fn sample_instrument_close() -> InstrumentClose {
3472 InstrumentClose::new(
3473 sample_instrument().id,
3474 Price::from("1.00000"),
3475 InstrumentCloseType::EndOfSession,
3476 UnixNanos::default(),
3477 UnixNanos::default(),
3478 )
3479 }
3480
3481 fn sample_option_greeks() -> OptionGreeks {
3482 OptionGreeks {
3483 instrument_id: InstrumentId::from("AUD/USD.SIM"),
3484 convention: GreeksConvention::BlackScholes,
3485 greeks: OptionGreekValues {
3486 delta: 0.55,
3487 gamma: 0.03,
3488 vega: 0.12,
3489 theta: -0.05,
3490 rho: 0.01,
3491 },
3492 mark_iv: Some(0.25),
3493 bid_iv: None,
3494 ask_iv: None,
3495 underlying_price: None,
3496 open_interest: None,
3497 ts_event: UnixNanos::default(),
3498 ts_init: UnixNanos::default(),
3499 }
3500 }
3501
3502 fn sample_option_chain() -> OptionChainSlice {
3503 OptionChainSlice {
3504 series_id: OptionSeriesId::new(
3505 Venue::from("SIM"),
3506 Ustr::from("AUD"),
3507 Ustr::from("USD"),
3508 UnixNanos::from(1_711_036_800_000_000_000),
3509 ),
3510 atm_strike: None,
3511 calls: BTreeMap::default(),
3512 puts: BTreeMap::default(),
3513 ts_event: UnixNanos::default(),
3514 ts_init: UnixNanos::default(),
3515 }
3516 }
3517
3518 fn sample_position_opened() -> PositionOpened {
3519 PositionOpened {
3520 trader_id: TraderId::from("TRADER-001"),
3521 strategy_id: StrategyId::from("TEST-001"),
3522 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
3523 position_id: PositionId::from("P-001"),
3524 account_id: AccountId::from("ACC-001"),
3525 opening_order_id: ClientOrderId::from("O-001"),
3526 entry: OrderSide::Buy,
3527 side: PositionSide::Long,
3528 signed_qty: 1.0,
3529 quantity: Quantity::from(1),
3530 last_qty: Quantity::from(1),
3531 last_px: Price::from("1.00000"),
3532 currency: Currency::from("USD"),
3533 avg_px_open: 1.0,
3534 event_id: UUID4::new(),
3535 ts_event: UnixNanos::default(),
3536 ts_init: UnixNanos::default(),
3537 }
3538 }
3539
3540 fn sample_position_changed() -> PositionChanged {
3541 PositionChanged {
3542 trader_id: TraderId::from("TRADER-001"),
3543 strategy_id: StrategyId::from("TEST-001"),
3544 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
3545 position_id: PositionId::from("P-001"),
3546 account_id: AccountId::from("ACC-001"),
3547 opening_order_id: ClientOrderId::from("O-001"),
3548 entry: OrderSide::Buy,
3549 side: PositionSide::Long,
3550 signed_qty: 2.0,
3551 quantity: Quantity::from(2),
3552 peak_quantity: Quantity::from(2),
3553 last_qty: Quantity::from(1),
3554 last_px: Price::from("1.10000"),
3555 currency: Currency::from("USD"),
3556 avg_px_open: 1.05,
3557 avg_px_close: None,
3558 realized_return: 0.0,
3559 realized_pnl: None,
3560 unrealized_pnl: Money::new(0.0, Currency::USD()),
3561 event_id: UUID4::new(),
3562 ts_opened: UnixNanos::default(),
3563 ts_event: UnixNanos::default(),
3564 ts_init: UnixNanos::default(),
3565 }
3566 }
3567
3568 fn sample_position_closed() -> PositionClosed {
3569 PositionClosed {
3570 trader_id: TraderId::from("TRADER-001"),
3571 strategy_id: StrategyId::from("TEST-001"),
3572 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
3573 position_id: PositionId::from("P-001"),
3574 account_id: AccountId::from("ACC-001"),
3575 opening_order_id: ClientOrderId::from("O-001"),
3576 closing_order_id: Some(ClientOrderId::from("O-002")),
3577 entry: OrderSide::Buy,
3578 side: PositionSide::Flat,
3579 signed_qty: 0.0,
3580 quantity: Quantity::from(0),
3581 peak_quantity: Quantity::from(2),
3582 last_qty: Quantity::from(2),
3583 last_px: Price::from("1.20000"),
3584 currency: Currency::from("USD"),
3585 avg_px_open: 1.05,
3586 avg_px_close: Some(1.20),
3587 realized_return: 0.1,
3588 realized_pnl: Some(Money::new(0.1, Currency::USD())),
3589 unrealized_pnl: Money::new(0.0, Currency::USD()),
3590 duration: 1,
3591 event_id: UUID4::new(),
3592 ts_opened: UnixNanos::default(),
3593 ts_closed: Some(UnixNanos::default()),
3594 ts_event: UnixNanos::default(),
3595 ts_init: UnixNanos::default(),
3596 }
3597 }
3598
3599 fn sample_python_market_order(
3600 py: Python<'_>,
3601 strategy_id: StrategyId,
3602 client_order_id: ClientOrderId,
3603 ) -> PyResult<Py<PyAny>> {
3604 let order = OrderTestBuilder::new(OrderType::Market)
3605 .trader_id(TraderId::from("TRADER-001"))
3606 .strategy_id(strategy_id)
3607 .instrument_id(sample_instrument().id)
3608 .client_order_id(client_order_id)
3609 .quantity(Quantity::from(100_000))
3610 .build();
3611
3612 order_any_to_pyobject(py, order)
3613 }
3614
3615 fn create_registered_tracking_strategy_with_config(
3616 py: Python<'_>,
3617 config: Option<StrategyConfig>,
3618 ) -> (Py<PyAny>, PyStrategy) {
3619 let py_strategy = create_tracking_python_strategy(py).unwrap();
3620 let mut rust_strategy = PyStrategy::new(config);
3621 rust_strategy.set_python_instance(py_strategy.clone_ref(py));
3622
3623 let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(TestClock::new()));
3624 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
3625 let portfolio = Rc::new(RefCell::new(Portfolio::new(
3626 clock.clone(),
3627 cache.clone(),
3628 None,
3629 )));
3630
3631 rust_strategy
3632 .register(TraderId::from("TRADER-001"), clock, cache, portfolio)
3633 .unwrap();
3634
3635 (py_strategy, rust_strategy)
3636 }
3637
3638 fn create_registered_tracking_strategy(py: Python<'_>) -> (Py<PyAny>, PyStrategy) {
3639 create_registered_tracking_strategy_with_config(py, None)
3640 }
3641
3642 #[rstest::rstest]
3643 fn test_external_order_claims_returns_configured_instruments() {
3644 let claims = vec![
3645 InstrumentId::from("AUDUSD.SIM"),
3646 InstrumentId::from("BTCUSDT.BINANCE"),
3647 ];
3648 let strategy = PyStrategy::new(Some(StrategyConfig {
3649 external_order_claims: Some(claims.clone()),
3650 ..Default::default()
3651 }));
3652
3653 assert_eq!(strategy.external_order_claims(), Some(claims));
3654 }
3655
3656 #[rstest::rstest]
3657 fn test_python_aggregate_event_handlers_exist() {
3658 pyo3::Python::initialize();
3659 Python::attach(|py| {
3660 let strategy = Py::new(py, PyStrategy::new(None)).unwrap();
3661 let strategy = strategy.bind(py);
3662
3663 assert!(strategy.hasattr("on_order_event").unwrap());
3664 assert!(strategy.hasattr("on_position_event").unwrap());
3665 });
3666 }
3667
3668 #[rstest::rstest]
3669 fn test_strategy_retains_python_config_object() {
3670 pyo3::Python::initialize();
3671 Python::attach(|py| {
3672 let config = py
3673 .eval(
3674 c_str!("type('_Cfg', (), {'strategy_id': 'S-RETAIN-001'})()"),
3675 None,
3676 None,
3677 )
3678 .unwrap();
3679
3680 let strategy = py
3681 .get_type::<PyStrategy>()
3682 .as_any()
3683 .call1((config.clone(),))
3684 .unwrap();
3685
3686 let retained = strategy.getattr("config").unwrap();
3687
3688 assert!(retained.is(&config));
3689 });
3690 }
3691
3692 #[rstest::rstest]
3693 fn test_indicator_registration_exposes_readiness_and_registered_view() {
3694 pyo3::Python::initialize();
3695 Python::attach(|py| {
3696 let mut rust_strategy = PyStrategy::new(None);
3697 let indicator = create_tracking_python_indicator(py).unwrap();
3698 let instrument_id = sample_instrument().id;
3699 let bar_type = sample_bar().bar_type;
3700
3701 assert_eq!(
3702 rust_strategy
3703 .py_registered_indicators(py)
3704 .unwrap()
3705 .bind(py)
3706 .len()
3707 .unwrap(),
3708 0
3709 );
3710 assert!(!rust_strategy.py_indicators_initialized(py).unwrap());
3711
3712 rust_strategy.py_register_indicator_for_quote_ticks(
3713 py,
3714 instrument_id,
3715 indicator.clone_ref(py),
3716 );
3717 rust_strategy.py_register_indicator_for_trade_ticks(
3718 py,
3719 instrument_id,
3720 indicator.clone_ref(py),
3721 );
3722 rust_strategy.py_register_indicator_for_bars(py, bar_type, indicator.clone_ref(py));
3723
3724 let registered = rust_strategy.py_registered_indicators(py).unwrap();
3725 let registered = registered.bind(py);
3726
3727 assert_eq!(registered.len().unwrap(), 1);
3728 assert_eq!(
3729 registered.get_item(0).unwrap().as_ptr(),
3730 indicator.bind(py).as_ptr()
3731 );
3732 assert!(!rust_strategy.py_indicators_initialized(py).unwrap());
3733
3734 indicator.bind(py).setattr("initialized", true).unwrap();
3735
3736 assert!(rust_strategy.py_indicators_initialized(py).unwrap());
3737 });
3738 }
3739
3740 #[rstest::rstest]
3741 fn test_registered_indicators_receive_quote_trade_and_bar_before_strategy_callbacks() {
3742 pyo3::Python::initialize();
3743 Python::attach(|py| {
3744 let events = PyList::empty(py);
3745 let py_strategy = create_indicator_event_strategy(py, &events).unwrap();
3746 let indicator = create_event_tracking_python_indicator(py, &events).unwrap();
3747
3748 let mut rust_strategy = PyStrategy::new(None);
3749 rust_strategy.set_python_instance(py_strategy.clone_ref(py));
3750
3751 let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(TestClock::new()));
3752 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
3753 let portfolio = Rc::new(RefCell::new(Portfolio::new(
3754 clock.clone(),
3755 cache.clone(),
3756 None,
3757 )));
3758
3759 rust_strategy
3760 .register(TraderId::from("TRADER-001"), clock, cache, portfolio)
3761 .unwrap();
3762 Component::start(rust_strategy.inner_mut()).unwrap();
3763
3764 let quote = sample_quote();
3765 let trade = sample_trade();
3766 let bar = sample_bar();
3767 let external_bar_type = BarType::from_str(&format!(
3768 "{}-1-MINUTE-LAST-EXTERNAL",
3769 bar.bar_type.instrument_id()
3770 ))
3771 .unwrap();
3772
3773 rust_strategy.py_register_indicator_for_quote_ticks(
3774 py,
3775 quote.instrument_id,
3776 indicator.clone_ref(py),
3777 );
3778 rust_strategy.py_register_indicator_for_trade_ticks(
3779 py,
3780 trade.instrument_id,
3781 indicator.clone_ref(py),
3782 );
3783 rust_strategy.py_register_indicator_for_bars(
3784 py,
3785 external_bar_type,
3786 indicator.clone_ref(py),
3787 );
3788
3789 DataActor::handle_quote(rust_strategy.inner_mut(), "e);
3790 DataActor::handle_trade(rust_strategy.inner_mut(), &trade);
3791 DataActor::handle_bar(rust_strategy.inner_mut(), &bar);
3792
3793 let events = events.extract::<Vec<String>>().unwrap();
3794
3795 assert_eq!(python_indicator_call_count(&indicator, py, "quote"), 1);
3796 assert_eq!(python_indicator_call_count(&indicator, py, "trade"), 1);
3797 assert_eq!(python_indicator_call_count(&indicator, py, "bar"), 1);
3798 assert_eq!(
3799 events,
3800 vec![
3801 "indicator:quote",
3802 "strategy:quote",
3803 "indicator:trade",
3804 "strategy:trade",
3805 "indicator:bar",
3806 "strategy:bar",
3807 ]
3808 );
3809 });
3810 }
3811
3812 #[rstest::rstest]
3813 fn test_registered_indicators_receive_historical_quote_trade_and_bar_batches() {
3814 pyo3::Python::initialize();
3815 Python::attach(|py| {
3816 let mut rust_strategy = PyStrategy::new(None);
3817 let indicator = create_tracking_python_indicator(py).unwrap();
3818 let quote = sample_quote();
3819 let trade = sample_trade();
3820 let bar = sample_bar();
3821 let quotes = vec![quote];
3822 let trades = vec![trade];
3823 let bars = vec![bar];
3824
3825 rust_strategy.py_register_indicator_for_quote_ticks(
3826 py,
3827 quote.instrument_id,
3828 indicator.clone_ref(py),
3829 );
3830 rust_strategy.py_register_indicator_for_trade_ticks(
3831 py,
3832 trade.instrument_id,
3833 indicator.clone_ref(py),
3834 );
3835 rust_strategy.py_register_indicator_for_bars(py, bar.bar_type, indicator.clone_ref(py));
3836
3837 let client_id = ClientId::new("TEST");
3838 let quotes_response = QuotesResponse::new(
3839 UUID4::new(),
3840 client_id,
3841 quote.instrument_id,
3842 quotes,
3843 None,
3844 None,
3845 UnixNanos::default(),
3846 None,
3847 );
3848 let trades_response = TradesResponse::new(
3849 UUID4::new(),
3850 client_id,
3851 trade.instrument_id,
3852 trades,
3853 None,
3854 None,
3855 UnixNanos::default(),
3856 None,
3857 );
3858 let bars_response = BarsResponse::new(
3859 UUID4::new(),
3860 client_id,
3861 bar.bar_type,
3862 bars,
3863 None,
3864 None,
3865 UnixNanos::default(),
3866 None,
3867 );
3868
3869 DataActor::handle_quotes_response(rust_strategy.inner_mut(), "es_response);
3870 DataActor::handle_trades_response(rust_strategy.inner_mut(), &trades_response);
3871 DataActor::handle_bars_response(rust_strategy.inner_mut(), &bars_response);
3872
3873 assert_eq!(python_indicator_call_count(&indicator, py, "quote"), 1);
3874 assert_eq!(python_indicator_call_count(&indicator, py, "trade"), 1);
3875 assert_eq!(python_indicator_call_count(&indicator, py, "bar"), 1);
3876 });
3877 }
3878
3879 fn assert_python_dispatch<F>(py: Python<'_>, method_name: &str, invoke: F)
3880 where
3881 F: FnOnce(&mut PyStrategy) -> anyhow::Result<()>,
3882 {
3883 let (py_strategy, mut rust_strategy) = create_registered_tracking_strategy(py);
3884 let result = invoke(&mut rust_strategy);
3885
3886 assert!(result.is_ok());
3887 assert!(python_method_was_called(&py_strategy, py, method_name));
3888 assert_eq!(python_method_call_count(&py_strategy, py, method_name), 1);
3889 }
3890
3891 #[rstest::rstest]
3892 #[case("on_start")]
3893 #[case("on_stop")]
3894 #[case("on_resume")]
3895 #[case("on_reset")]
3896 #[case("on_dispose")]
3897 #[case("on_degrade")]
3898 #[case("on_fault")]
3899 fn test_python_dispatch_lifecycle_matrix(#[case] method_name: &str) {
3900 pyo3::Python::initialize();
3901 Python::attach(|py| {
3902 assert_python_dispatch(py, method_name, |rust_strategy| match method_name {
3903 "on_start" => DataActor::on_start(rust_strategy.inner_mut()),
3904 "on_stop" => DataActor::on_stop(rust_strategy.inner_mut()),
3905 "on_resume" => DataActor::on_resume(rust_strategy.inner_mut()),
3906 "on_reset" => DataActor::on_reset(rust_strategy.inner_mut()),
3907 "on_dispose" => DataActor::on_dispose(rust_strategy.inner_mut()),
3908 "on_degrade" => DataActor::on_degrade(rust_strategy.inner_mut()),
3909 "on_fault" => DataActor::on_fault(rust_strategy.inner_mut()),
3910 _ => unreachable!("unhandled lifecycle case: {method_name}"),
3911 });
3912 });
3913 }
3914
3915 #[rstest::rstest]
3916 #[case("on_save")]
3917 #[case("on_load")]
3918 fn test_python_dispatch_persistence_matrix(#[case] method_name: &str) {
3919 pyo3::Python::initialize();
3920 Python::attach(|py| {
3921 assert_python_dispatch(py, method_name, |rust_strategy| match method_name {
3922 "on_save" => {
3923 let state = DataActor::on_save(rust_strategy.inner()).unwrap();
3924 assert_eq!(
3925 state.get("strategy").map(Vec::as_slice),
3926 Some(b"saved".as_slice())
3927 );
3928 Ok(())
3929 }
3930 "on_load" => {
3931 let mut state = IndexMap::new();
3932 state.insert("strategy".to_string(), b"loaded".to_vec());
3933 DataActor::on_load(rust_strategy.inner_mut(), state)
3934 }
3935 _ => unreachable!("unhandled persistence case: {method_name}"),
3936 });
3937 });
3938 }
3939
3940 #[rstest::rstest]
3941 fn test_python_persistence_methods_convert_state() {
3942 pyo3::Python::initialize();
3943 Python::attach(|py| {
3944 let (py_strategy, mut rust_strategy) = create_registered_tracking_strategy(py);
3945
3946 let saved = rust_strategy.py_save(py).unwrap();
3947 let saved_state = saved
3948 .bind(py)
3949 .extract::<HashMap<String, Vec<u8>>>()
3950 .unwrap();
3951 assert_eq!(
3952 saved_state.get("strategy").map(Vec::as_slice),
3953 Some(&b"saved"[..])
3954 );
3955
3956 let load_state = PyDict::new(py);
3957 load_state
3958 .set_item("strategy", PyBytes::new(py, b"loaded-from-python"))
3959 .unwrap();
3960
3961 rust_strategy.py_load(&load_state).unwrap();
3962
3963 let loaded_state = python_last_loaded_state(&py_strategy, py).unwrap();
3964 assert_eq!(
3965 loaded_state.get("strategy").map(Vec::as_slice),
3966 Some(&b"loaded-from-python"[..])
3967 );
3968 });
3969 }
3970
3971 #[rstest::rstest]
3972 fn test_python_stop_stops_immediately_when_manage_stop_disabled() {
3973 pyo3::Python::initialize();
3974 Python::attach(|py| {
3975 let config = StrategyConfig {
3976 strategy_id: Some(StrategyId::from("TEST-001")),
3977 order_id_tag: Some("001".to_string()),
3978 manage_stop: false,
3979 ..Default::default()
3980 };
3981 let (py_strategy, mut rust_strategy) =
3982 create_registered_tracking_strategy_with_config(py, Some(config));
3983
3984 rust_strategy.py_start().unwrap();
3985 rust_strategy.py_stop().unwrap();
3986
3987 assert!(rust_strategy.py_is_stopped());
3988 assert!(!rust_strategy.inner().core.pending_stop);
3989 assert!(!rust_strategy.inner().core.is_exiting);
3990 assert_eq!(python_method_call_count(&py_strategy, py, "on_stop"), 1);
3991 });
3992 }
3993
3994 #[rstest::rstest]
3995 fn test_python_stop_defers_when_manage_stop_enabled() {
3996 pyo3::Python::initialize();
3997 Python::attach(|py| {
3998 let config = StrategyConfig {
3999 strategy_id: Some(StrategyId::from("TEST-001")),
4000 order_id_tag: Some("001".to_string()),
4001 manage_stop: true,
4002 ..Default::default()
4003 };
4004 let (py_strategy, mut rust_strategy) =
4005 create_registered_tracking_strategy_with_config(py, Some(config));
4006
4007 rust_strategy.py_start().unwrap();
4008 rust_strategy.py_stop().unwrap();
4009
4010 assert!(rust_strategy.py_is_running());
4011 assert!(rust_strategy.inner().core.pending_stop);
4012 assert!(rust_strategy.inner().core.is_exiting);
4013 assert_eq!(python_method_call_count(&py_strategy, py, "on_stop"), 0);
4014 });
4015 }
4016
4017 #[rstest::rstest]
4018 fn test_python_market_exit_methods_update_state_and_dispatch_hooks() {
4019 pyo3::Python::initialize();
4020 Python::attach(|py| {
4021 let (py_strategy, mut rust_strategy) = create_registered_tracking_strategy(py);
4022
4023 rust_strategy.py_start().unwrap();
4024
4025 assert!(!rust_strategy.py_is_exiting());
4026
4027 rust_strategy.py_market_exit().unwrap();
4028
4029 assert!(rust_strategy.py_is_exiting());
4030 assert_eq!(
4031 python_method_call_count(&py_strategy, py, "on_market_exit"),
4032 1
4033 );
4034
4035 rust_strategy.inner_mut().finalize_market_exit();
4036
4037 assert!(!rust_strategy.py_is_exiting());
4038 assert_eq!(
4039 python_method_call_count(&py_strategy, py, "post_market_exit"),
4040 1
4041 );
4042 });
4043 }
4044
4045 #[rstest::rstest]
4046 #[case::order_list_object(true)]
4047 #[case::raw_order_sequence(false)]
4048 fn test_python_submit_order_list_accepts_order_list_inputs(#[case] wrap_order_list: bool) {
4049 pyo3::Python::initialize();
4050 Python::attach(|py| {
4051 let (_, mut rust_strategy) = create_registered_tracking_strategy(py);
4052 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4053 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4054 msgbus::register_trading_command_endpoint(
4055 MessagingSwitchboard::risk_engine_queue_execute(),
4056 risk_handler,
4057 );
4058
4059 let strategy_id = rust_strategy.strategy_id();
4060 let client_order_id1 = ClientOrderId::from("O-PYO3-LIST-001");
4061 let client_order_id2 = ClientOrderId::from("O-PYO3-LIST-002");
4062 let orders = vec![
4063 sample_python_market_order(py, strategy_id, client_order_id1).unwrap(),
4064 sample_python_market_order(py, strategy_id, client_order_id2).unwrap(),
4065 ];
4066 let params = PyDict::new(py);
4067
4068 params.set_item("routing_hint", "prefer_batch").unwrap();
4069 let order_list = if wrap_order_list {
4070 let order_list_type = py
4071 .eval(c_str!("type('OrderListShim', (), {})"), None, None)
4072 .unwrap();
4073 let order_list = order_list_type.call0().unwrap();
4074
4075 order_list.setattr("orders", orders).unwrap();
4076 order_list.unbind()
4077 } else {
4078 PyList::new(py, orders).unwrap().into_any().unbind()
4079 };
4080
4081 rust_strategy
4082 .py_submit_order_list(py, order_list, None, None, Some(params.unbind()))
4083 .unwrap();
4084
4085 let cache = DataActor::cache(rust_strategy.inner());
4086 let cached_order1 = cache.order(&client_order_id1).unwrap();
4087 let cached_order2 = cache.order(&client_order_id2).unwrap();
4088 let order_list_id = cached_order1.order_list_id().unwrap();
4089 let order_list = cache.order_list(&order_list_id).unwrap();
4090
4091 assert_eq!(cached_order2.order_list_id(), Some(order_list_id));
4092 assert_eq!(
4093 order_list.client_order_ids.as_slice(),
4094 &[client_order_id1, client_order_id2]
4095 );
4096
4097 let risk_messages = risk_messages.get_messages();
4098 assert_eq!(risk_messages.len(), 1);
4099 let Some(TradingCommand::SubmitOrderList(command)) = risk_messages.first() else {
4100 panic!("expected SubmitOrderList command");
4101 };
4102 assert_eq!(
4103 command
4104 .params
4105 .as_ref()
4106 .and_then(|params| params.get("routing_hint")),
4107 Some(&Value::String("prefer_batch".to_string()))
4108 );
4109 });
4110 }
4111
4112 #[rstest::rstest]
4113 fn test_python_cancel_gtd_expiry_accepts_order() {
4114 pyo3::Python::initialize();
4115 Python::attach(|py| {
4116 let (_, mut rust_strategy) = create_registered_tracking_strategy(py);
4117 let strategy_id = rust_strategy.strategy_id();
4118 let client_order_id = ClientOrderId::from("O-PYO3-GTD-001");
4119 let timer_name = format!("GTD-EXPIRY:{client_order_id}");
4120 let order = OrderTestBuilder::new(OrderType::Limit)
4121 .trader_id(TraderId::from("TRADER-001"))
4122 .strategy_id(strategy_id)
4123 .instrument_id(sample_instrument().id)
4124 .client_order_id(client_order_id)
4125 .quantity(Quantity::from(100_000))
4126 .price(Price::from("1.00000"))
4127 .time_in_force(TimeInForce::Gtd)
4128 .expire_time(UnixNanos::from(1))
4129 .build();
4130 let py_order = order_any_to_pyobject(py, order).unwrap();
4131
4132 {
4133 let mut clock = rust_strategy.inner_mut().core.clock_mut();
4134 clock
4135 .set_time_alert_ns(&timer_name, UnixNanos::from(1), None, None)
4136 .unwrap();
4137 }
4138 rust_strategy
4139 .inner_mut()
4140 .core
4141 .gtd_timers
4142 .insert(client_order_id, Ustr::from(&timer_name));
4143
4144 rust_strategy
4145 .py_cancel_gtd_expiry(py, py_order)
4146 .expect("cancel_gtd_expiry should accept Python order");
4147
4148 let clock_timer_exists = rust_strategy
4149 .inner_mut()
4150 .core
4151 .clock_mut()
4152 .timer_names()
4153 .contains(&timer_name.as_str());
4154
4155 assert!(
4156 !rust_strategy
4157 .inner_mut()
4158 .has_gtd_expiry_timer(&client_order_id)
4159 );
4160 assert!(!clock_timer_exists);
4161 });
4162 }
4163
4164 #[rstest::rstest]
4165 #[case("on_time_event")]
4166 #[case("on_data")]
4167 #[case("on_signal")]
4168 #[case("on_instrument")]
4169 #[case("on_quote")]
4170 #[case("on_trade")]
4171 #[case("on_bar")]
4172 #[case("on_book_deltas")]
4173 #[case("on_book")]
4174 #[case("on_mark_price")]
4175 #[case("on_index_price")]
4176 #[case("on_funding_rate")]
4177 #[case("on_instrument_status")]
4178 #[case("on_instrument_close")]
4179 #[case("on_option_greeks")]
4180 #[case("on_option_chain")]
4181 #[case("on_historical_data")]
4182 #[case("on_historical_quotes")]
4183 #[case("on_historical_trades")]
4184 #[case("on_historical_funding_rates")]
4185 #[case("on_historical_bars")]
4186 #[case("on_historical_mark_prices")]
4187 #[case("on_historical_index_prices")]
4188 fn test_python_dispatch_data_callback_matrix(#[case] method_name: &str) {
4189 pyo3::Python::initialize();
4190 Python::attach(|py| {
4191 assert_python_dispatch(py, method_name, |rust_strategy| match method_name {
4192 "on_time_event" => {
4193 let event = sample_time_event();
4194 DataActor::on_time_event(rust_strategy.inner_mut(), &event)
4195 }
4196 "on_data" => {
4197 let data = sample_data();
4198 rust_strategy.inner_mut().on_data(&data)
4199 }
4200 "on_signal" => {
4201 let signal = sample_signal();
4202 rust_strategy.inner_mut().on_signal(&signal)
4203 }
4204 "on_instrument" => {
4205 let instrument = InstrumentAny::CurrencyPair(sample_instrument());
4206 rust_strategy.inner_mut().on_instrument(&instrument)
4207 }
4208 "on_quote" => {
4209 let quote = sample_quote();
4210 rust_strategy.inner_mut().on_quote("e)
4211 }
4212 "on_trade" => {
4213 let trade = sample_trade();
4214 rust_strategy.inner_mut().on_trade(&trade)
4215 }
4216 "on_bar" => {
4217 let bar = sample_bar();
4218 rust_strategy.inner_mut().on_bar(&bar)
4219 }
4220 "on_book_deltas" => {
4221 let deltas = sample_book_deltas();
4222 rust_strategy.inner_mut().on_book_deltas(&deltas)
4223 }
4224 "on_book" => {
4225 let book = sample_book();
4226 rust_strategy.inner_mut().on_book(&book)
4227 }
4228 "on_mark_price" => {
4229 let mark_price = sample_mark_price();
4230 rust_strategy.inner_mut().on_mark_price(&mark_price)
4231 }
4232 "on_index_price" => {
4233 let index_price = sample_index_price();
4234 rust_strategy.inner_mut().on_index_price(&index_price)
4235 }
4236 "on_funding_rate" => {
4237 let funding_rate = sample_funding_rate();
4238 rust_strategy.inner_mut().on_funding_rate(&funding_rate)
4239 }
4240 "on_instrument_status" => {
4241 let status = sample_instrument_status();
4242 rust_strategy.inner_mut().on_instrument_status(&status)
4243 }
4244 "on_instrument_close" => {
4245 let close = sample_instrument_close();
4246 rust_strategy.inner_mut().on_instrument_close(&close)
4247 }
4248 "on_option_greeks" => {
4249 let greeks = sample_option_greeks();
4250 DataActor::on_option_greeks(rust_strategy.inner_mut(), &greeks)
4251 }
4252 "on_option_chain" => {
4253 let slice = sample_option_chain();
4254 DataActor::on_option_chain(rust_strategy.inner_mut(), &slice)
4255 }
4256 "on_historical_data" => {
4257 let data = sample_data();
4258 rust_strategy.inner_mut().on_historical_data(&data)
4259 }
4260 "on_historical_quotes" => {
4261 let quotes = vec![sample_quote()];
4262 rust_strategy.inner_mut().on_historical_quotes("es)
4263 }
4264 "on_historical_trades" => {
4265 let trades = vec![sample_trade()];
4266 rust_strategy.inner_mut().on_historical_trades(&trades)
4267 }
4268 "on_historical_funding_rates" => {
4269 let funding_rates = vec![sample_funding_rate()];
4270 rust_strategy
4271 .inner_mut()
4272 .on_historical_funding_rates(&funding_rates)
4273 }
4274 "on_historical_bars" => {
4275 let bars = vec![sample_bar()];
4276 rust_strategy.inner_mut().on_historical_bars(&bars)
4277 }
4278 "on_historical_mark_prices" => {
4279 let mark_prices = vec![sample_mark_price()];
4280 rust_strategy
4281 .inner_mut()
4282 .on_historical_mark_prices(&mark_prices)
4283 }
4284 "on_historical_index_prices" => {
4285 let index_prices = vec![sample_index_price()];
4286 rust_strategy
4287 .inner_mut()
4288 .on_historical_index_prices(&index_prices)
4289 }
4290 _ => unreachable!("unhandled data callback case: {method_name}"),
4291 });
4292 });
4293 }
4294
4295 #[rstest::rstest]
4296 #[case("on_order_initialized")]
4297 #[case("on_order_event")]
4298 #[case("on_order_denied")]
4299 #[case("on_order_emulated")]
4300 #[case("on_order_released")]
4301 #[case("on_order_submitted")]
4302 #[case("on_order_rejected")]
4303 #[case("on_order_accepted")]
4304 #[case("on_order_expired")]
4305 #[case("on_order_triggered")]
4306 #[case("on_order_pending_update")]
4307 #[case("on_order_pending_cancel")]
4308 #[case("on_order_modify_rejected")]
4309 #[case("on_order_cancel_rejected")]
4310 #[case("on_order_updated")]
4311 #[case("on_order_canceled")]
4312 #[case("on_order_filled")]
4313 fn test_python_dispatch_order_callback_matrix(#[case] method_name: &str) {
4314 pyo3::Python::initialize();
4315 Python::attach(|py| {
4316 assert_python_dispatch(py, method_name, |rust_strategy| match method_name {
4317 "on_order_initialized" => {
4318 Strategy::on_order_initialized(
4319 rust_strategy.inner_mut(),
4320 OrderInitialized::default(),
4321 );
4322 Ok(())
4323 }
4324 "on_order_event" => {
4325 Strategy::on_order_event(
4326 rust_strategy.inner_mut(),
4327 OrderEventAny::Accepted(OrderAccepted::default()),
4328 );
4329 Ok(())
4330 }
4331 "on_order_denied" => {
4332 Strategy::on_order_denied(rust_strategy.inner_mut(), OrderDenied::default());
4333 Ok(())
4334 }
4335 "on_order_emulated" => {
4336 Strategy::on_order_emulated(
4337 rust_strategy.inner_mut(),
4338 OrderEmulated::default(),
4339 );
4340 Ok(())
4341 }
4342 "on_order_released" => {
4343 Strategy::on_order_released(
4344 rust_strategy.inner_mut(),
4345 OrderReleased::default(),
4346 );
4347 Ok(())
4348 }
4349 "on_order_submitted" => {
4350 Strategy::on_order_submitted(
4351 rust_strategy.inner_mut(),
4352 OrderSubmitted::default(),
4353 );
4354 Ok(())
4355 }
4356 "on_order_rejected" => {
4357 Strategy::on_order_rejected(
4358 rust_strategy.inner_mut(),
4359 OrderRejected::default(),
4360 );
4361 Ok(())
4362 }
4363 "on_order_accepted" => {
4364 Strategy::on_order_accepted(
4365 rust_strategy.inner_mut(),
4366 OrderAccepted::default(),
4367 );
4368 Ok(())
4369 }
4370 "on_order_expired" => {
4371 Strategy::on_order_expired(rust_strategy.inner_mut(), OrderExpired::default());
4372 Ok(())
4373 }
4374 "on_order_triggered" => {
4375 Strategy::on_order_triggered(
4376 rust_strategy.inner_mut(),
4377 OrderTriggered::default(),
4378 );
4379 Ok(())
4380 }
4381 "on_order_pending_update" => {
4382 Strategy::on_order_pending_update(
4383 rust_strategy.inner_mut(),
4384 OrderPendingUpdate::default(),
4385 );
4386 Ok(())
4387 }
4388 "on_order_pending_cancel" => {
4389 Strategy::on_order_pending_cancel(
4390 rust_strategy.inner_mut(),
4391 OrderPendingCancel::default(),
4392 );
4393 Ok(())
4394 }
4395 "on_order_modify_rejected" => {
4396 Strategy::on_order_modify_rejected(
4397 rust_strategy.inner_mut(),
4398 OrderModifyRejected::default(),
4399 );
4400 Ok(())
4401 }
4402 "on_order_cancel_rejected" => {
4403 Strategy::on_order_cancel_rejected(
4404 rust_strategy.inner_mut(),
4405 OrderCancelRejected::default(),
4406 );
4407 Ok(())
4408 }
4409 "on_order_updated" => {
4410 Strategy::on_order_updated(rust_strategy.inner_mut(), OrderUpdated::default());
4411 Ok(())
4412 }
4413 "on_order_canceled" => {
4414 let event = OrderCanceled::default();
4415 DataActor::on_order_canceled(rust_strategy.inner_mut(), &event)
4416 }
4417 "on_order_filled" => {
4418 let event = OrderFilledSpec::builder().build();
4419 DataActor::on_order_filled(rust_strategy.inner_mut(), &event)
4420 }
4421 _ => unreachable!("unhandled order callback case: {method_name}"),
4422 });
4423 });
4424 }
4425
4426 #[rstest::rstest]
4427 fn test_python_handle_order_event_dispatches_specific_and_aggregate_callbacks() {
4428 pyo3::Python::initialize();
4429 Python::attach(|py| {
4430 let (py_strategy, mut rust_strategy) = create_registered_tracking_strategy(py);
4431
4432 rust_strategy.py_start().unwrap();
4433 Strategy::handle_order_event(
4434 rust_strategy.inner_mut(),
4435 OrderEventAny::Accepted(OrderAccepted::default()),
4436 );
4437
4438 assert_eq!(
4439 python_method_call_count(&py_strategy, py, "on_order_accepted"),
4440 1
4441 );
4442 assert_eq!(
4443 python_method_call_count(&py_strategy, py, "on_order_event"),
4444 1
4445 );
4446 let call_names = python_method_call_names(&py_strategy, py);
4447 assert_eq!(
4448 &call_names[call_names.len() - 2..],
4449 ["on_order_accepted", "on_order_event"],
4450 );
4451 });
4452 }
4453
4454 #[rstest::rstest]
4455 #[case("on_position_event")]
4456 #[case("on_position_opened")]
4457 #[case("on_position_changed")]
4458 #[case("on_position_closed")]
4459 fn test_python_dispatch_position_callback_matrix(#[case] method_name: &str) {
4460 pyo3::Python::initialize();
4461 Python::attach(|py| {
4462 assert_python_dispatch(py, method_name, |rust_strategy| match method_name {
4463 "on_position_event" => {
4464 Strategy::on_position_event(
4465 rust_strategy.inner_mut(),
4466 PositionEvent::PositionOpened(sample_position_opened()),
4467 );
4468 Ok(())
4469 }
4470 "on_position_opened" => {
4471 Strategy::on_position_opened(
4472 rust_strategy.inner_mut(),
4473 sample_position_opened(),
4474 );
4475 Ok(())
4476 }
4477 "on_position_changed" => {
4478 Strategy::on_position_changed(
4479 rust_strategy.inner_mut(),
4480 sample_position_changed(),
4481 );
4482 Ok(())
4483 }
4484 "on_position_closed" => {
4485 Strategy::on_position_closed(
4486 rust_strategy.inner_mut(),
4487 sample_position_closed(),
4488 );
4489 Ok(())
4490 }
4491 _ => unreachable!("unhandled position callback case: {method_name}"),
4492 });
4493 });
4494 }
4495
4496 #[rstest::rstest]
4497 fn test_python_handle_position_event_dispatches_specific_and_aggregate_callbacks() {
4498 pyo3::Python::initialize();
4499 Python::attach(|py| {
4500 let (py_strategy, mut rust_strategy) = create_registered_tracking_strategy(py);
4501
4502 rust_strategy.py_start().unwrap();
4503 Strategy::handle_position_event(
4504 rust_strategy.inner_mut(),
4505 PositionEvent::PositionOpened(sample_position_opened()),
4506 );
4507
4508 assert_eq!(
4509 python_method_call_count(&py_strategy, py, "on_position_opened"),
4510 1
4511 );
4512 assert_eq!(
4513 python_method_call_count(&py_strategy, py, "on_position_event"),
4514 1
4515 );
4516 let call_names = python_method_call_names(&py_strategy, py);
4517 assert_eq!(
4518 &call_names[call_names.len() - 2..],
4519 ["on_position_opened", "on_position_event"],
4520 );
4521 });
4522 }
4523}