1use std::{
17 any::Any,
18 cell::{RefCell, UnsafeCell},
19 collections::HashMap,
20 fmt::Debug,
21 num::NonZeroUsize,
22 ops::{Deref, DerefMut},
23 rc::Rc,
24};
25
26use indexmap::IndexMap;
27use jiff::Timestamp;
28use nautilus_core::{
29 from_pydict,
30 nanos::UnixNanos,
31 python::{to_pyruntime_err, to_pyvalue_err, upgrade_py_weakref},
32};
33#[cfg(feature = "defi")]
34use nautilus_model::defi::{
35 Block, Blockchain, Pool, PoolFeeCollect, PoolFlash, PoolLiquidityUpdate, PoolSwap,
36};
37use nautilus_model::{
38 data::{
39 Bar, BarType, CustomData, DataType, FundingRateUpdate, IndexPriceUpdate, InstrumentStatus,
40 MarkPriceUpdate, OrderBookDelta, OrderBookDeltas, OrderBookDepth, QuoteTick, TradeTick,
41 close::InstrumentClose,
42 option_chain::{OptionChainSlice, OptionGreeks},
43 },
44 enums::BookType,
45 identifiers::{
46 ActorId, ClientId, ExecAlgorithmId, InstrumentId, OptionSeriesId, TraderId, Venue,
47 },
48 instruments::{InstrumentAny, SyntheticInstrument},
49 orderbook::OrderBook,
50 orders::{OrderAny, OrderList},
51 python::{
52 data::option_chain::PyStrikeRange, instruments::instrument_any_to_pyobject,
53 orders::order_any_to_pyobject,
54 },
55};
56use pyo3::{
57 IntoPyObjectExt,
58 prelude::*,
59 types::{PyBytes, PyDict, PyList, PyWeakrefReference},
60};
61use ustr::Ustr;
62
63use crate::{
64 actor::{
65 Actor, DataActor, DataActorNative,
66 data_actor::{DataActorConfig, DataActorCore, ImportableActorConfig},
67 registry::{try_get_actor_unchecked, with_actor_registry},
68 },
69 cache::Cache,
70 clock::Clock,
71 component::{Component, ComponentAccessError, with_component_registry},
72 enums::ComponentState,
73 logging::{CMD, RECV},
74 messages::{
75 execution::TradingCommand,
76 system::{QueueStateChanged, SocketStateChanged},
77 },
78 msgbus::{self, ShareableMessageHandler},
79 python::{
80 cache::PyCache,
81 clock::PyClock,
82 indicators::{registered_python_indicators, wrap_python_indicator},
83 logging::{PyLogger, format_exception},
84 wrappers::{get_python_message_bus, retain_python_wrapper},
85 },
86 runner::SystemChannel,
87 signal::Signal,
88 timer::{TimeEvent, TimeEventCallback},
89};
90
91#[pyo3::pymethods]
92#[pyo3_stub_gen::derive::gen_stub_pymethods]
93impl DataActorConfig {
94 #[new]
96 #[pyo3(signature = (actor_id=None, log_events=true, log_commands=true, **_kwargs))]
97 fn py_new(
98 actor_id: Option<ActorId>,
99 log_events: bool,
100 log_commands: bool,
101 _kwargs: Option<&Bound<'_, PyDict>>,
102 ) -> Self {
103 Self {
104 actor_id,
105 log_events,
106 log_commands,
107 }
108 }
109
110 #[getter]
111 fn actor_id(&self) -> Option<ActorId> {
112 self.actor_id
113 }
114
115 #[setter]
116 fn set_actor_id(&mut self, actor_id: Option<ActorId>) {
117 self.actor_id = actor_id;
118 }
119
120 #[getter]
121 fn log_events(&self) -> bool {
122 self.log_events
123 }
124
125 #[setter]
126 fn set_log_events(&mut self, log_events: bool) {
127 self.log_events = log_events;
128 }
129
130 #[getter]
131 fn log_commands(&self) -> bool {
132 self.log_commands
133 }
134
135 #[setter]
136 fn set_log_commands(&mut self, log_commands: bool) {
137 self.log_commands = log_commands;
138 }
139}
140
141#[pyo3::pymethods]
142#[pyo3_stub_gen::derive::gen_stub_pymethods]
143impl ImportableActorConfig {
144 #[new]
146 #[expect(clippy::needless_pass_by_value)]
147 fn py_new(actor_path: String, config_path: String, config: Py<PyDict>) -> PyResult<Self> {
148 let json_config = Python::attach(|py| -> PyResult<HashMap<String, serde_json::Value>> {
149 let kwargs = PyDict::new(py);
150 kwargs.set_item("default", py.eval(pyo3::ffi::c_str!("str"), None, None)?)?;
151 let json_str: String = PyModule::import(py, "json")?
152 .call_method("dumps", (config.bind(py),), Some(&kwargs))?
153 .extract()?;
154
155 let json_value: serde_json::Value =
156 serde_json::from_str(&json_str).map_err(to_pyvalue_err)?;
157
158 if let serde_json::Value::Object(map) = json_value {
159 Ok(map.into_iter().collect())
160 } else {
161 Err(to_pyvalue_err("Config must be a dictionary"))
162 }
163 })?;
164
165 Ok(Self {
166 actor_path,
167 config_path,
168 config: json_config,
169 })
170 }
171
172 #[getter]
173 fn actor_path(&self) -> &String {
174 &self.actor_path
175 }
176
177 #[getter]
178 fn config_path(&self) -> &String {
179 &self.config_path
180 }
181
182 #[getter]
183 fn config(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
184 let py_dict = PyDict::new(py);
186
187 for (key, value) in &self.config {
188 let json_str = serde_json::to_string(value).map_err(to_pyvalue_err)?;
190 let py_value = PyModule::import(py, "json")?.call_method("loads", (json_str,), None)?;
191 py_dict.set_item(key, py_value)?;
192 }
193 Ok(py_dict.unbind())
194 }
195}
196
197pub struct PyDataActorInner {
203 core: DataActorCore,
204 py_self: Option<Py<PyWeakrefReference>>,
205 config: Option<Py<PyAny>>,
206 clock: PyClock,
207 logger: PyLogger,
208}
209
210impl Debug for PyDataActorInner {
211 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212 f.debug_struct(stringify!(PyDataActorInner))
213 .field("core", &self.core)
214 .field(
215 "py_self",
216 &self.py_self.as_ref().map(|_| "<Py<PyWeakrefReference>>"),
217 )
218 .field("config", &self.config.as_ref().map(|_| "<Py<PyAny>>"))
219 .field("clock", &self.clock)
220 .field("logger", &self.logger)
221 .finish()
222 }
223}
224
225impl Deref for PyDataActorInner {
226 type Target = DataActorCore;
227
228 fn deref(&self) -> &Self::Target {
229 &self.core
230 }
231}
232
233impl DerefMut for PyDataActorInner {
234 fn deref_mut(&mut self) -> &mut Self::Target {
235 &mut self.core
236 }
237}
238
239impl DataActorNative for PyDataActorInner {
240 fn core(&self) -> &DataActorCore {
241 &self.core
242 }
243
244 fn core_mut(&mut self) -> &mut DataActorCore {
245 &mut self.core
246 }
247}
248
249#[expect(clippy::needless_pass_by_ref_mut)]
250impl PyDataActorInner {
251 fn execute_exec_algorithm_command(&mut self, command: &TradingCommand) -> anyhow::Result<()> {
252 if self.core.config.log_commands {
253 let id = self.core.actor_id;
254 log::info!("{id} {RECV}{CMD} {command}");
255 }
256
257 if self.core.state() != ComponentState::Running {
258 return Ok(());
259 }
260
261 match command {
262 TradingCommand::SubmitOrder(cmd) => {
263 let order = DataActor::cache(self).try_order(&cmd.client_order_id)?;
264 self.dispatch_on_order(order).map_err(|e| {
265 anyhow::anyhow!("Python on_order failed:\n{}", format_exception(&e))
266 })
267 }
268 TradingCommand::SubmitOrderList(cmd) => {
269 let orders = self.orders_for_list(&cmd.order_list)?;
270 self.dispatch_on_order_list(cmd.order_list.clone(), orders)
271 .map_err(|e| {
272 anyhow::anyhow!("Python on_order_list failed:\n{}", format_exception(&e))
273 })
274 }
275 _ => {
276 log::warn!("Unhandled command type: {command}");
277 Ok(())
278 }
279 }
280 }
281
282 fn orders_for_list(&self, order_list: &OrderList) -> anyhow::Result<Vec<OrderAny>> {
283 let cache = DataActor::cache(self);
284 let mut orders = Vec::with_capacity(order_list.client_order_ids.len());
285
286 for client_order_id in &order_list.client_order_ids {
287 orders.push(cache.try_order(client_order_id)?);
288 }
289
290 Ok(orders)
291 }
292
293 fn dispatch_on_order(&mut self, order: OrderAny) -> PyResult<()> {
294 if let Some(py_self) = self.python_instance()? {
295 Python::attach(|py| -> PyResult<()> {
296 let py_order = order_any_to_pyobject(py, order)?;
297 py_self.call_method1(py, "on_order", (py_order,))?;
298 Ok(())
299 })?;
300 }
301 Ok(())
302 }
303
304 fn dispatch_on_order_list(
305 &mut self,
306 order_list: OrderList,
307 orders: Vec<OrderAny>,
308 ) -> PyResult<()> {
309 if let Some(py_self) = self.python_instance()? {
310 Python::attach(|py| -> PyResult<()> {
311 if py_self.bind(py).hasattr("on_order_list")? {
312 let py_order_list = order_list.into_py_any(py)?;
313 let py_orders = orders
314 .into_iter()
315 .map(|order| order_any_to_pyobject(py, order))
316 .collect::<PyResult<Vec<_>>>()?;
317 let py_orders = PyList::new(py, py_orders)?;
318
319 py_self.call_method1(py, "on_order_list", (py_order_list, py_orders))?;
320 } else {
321 for order in orders {
322 let py_order = order_any_to_pyobject(py, order)?;
323 py_self.call_method1(py, "on_order", (py_order,))?;
324 }
325 }
326 Ok(())
327 })?;
328 }
329 Ok(())
330 }
331
332 fn dispatch_on_start(&self) -> PyResult<()> {
333 if let Some(py_self) = self.python_instance()? {
334 Python::attach(|py| py_self.call_method0(py, "on_start"))?;
335 }
336 Ok(())
337 }
338
339 fn dispatch_on_stop(&mut self) -> PyResult<()> {
340 if let Some(py_self) = self.python_instance()? {
341 Python::attach(|py| py_self.call_method0(py, "on_stop"))?;
342 }
343 Ok(())
344 }
345
346 fn dispatch_on_resume(&mut self) -> PyResult<()> {
347 if let Some(py_self) = self.python_instance()? {
348 Python::attach(|py| py_self.call_method0(py, "on_resume"))?;
349 }
350 Ok(())
351 }
352
353 fn dispatch_on_reset(&mut self) -> PyResult<()> {
354 if let Some(py_self) = self.python_instance()? {
355 Python::attach(|py| py_self.call_method0(py, "on_reset"))?;
356 }
357 Ok(())
358 }
359
360 fn dispatch_on_dispose(&mut self) -> PyResult<()> {
361 if let Some(py_self) = self.python_instance()? {
362 Python::attach(|py| py_self.call_method0(py, "on_dispose"))?;
363 }
364 Ok(())
365 }
366
367 fn dispatch_on_degrade(&mut self) -> PyResult<()> {
368 if let Some(py_self) = self.python_instance()? {
369 Python::attach(|py| py_self.call_method0(py, "on_degrade"))?;
370 }
371 Ok(())
372 }
373
374 fn dispatch_on_fault(&mut self) -> PyResult<()> {
375 if let Some(py_self) = self.python_instance()? {
376 Python::attach(|py| py_self.call_method0(py, "on_fault"))?;
377 }
378 Ok(())
379 }
380
381 fn dispatch_on_save(&self) -> PyResult<IndexMap<String, Vec<u8>>> {
382 if let Some(py_self) = self.python_instance()? {
383 Python::attach(|py| {
384 let py_state = py_self.call_method0(py, "on_save")?;
385 let py_state: &Bound<'_, PyDict> = py_state.cast_bound::<PyDict>(py)?;
386 pydict_to_state(py_state)
387 })
388 } else {
389 Ok(IndexMap::new())
390 }
391 }
392
393 fn dispatch_on_load(&mut self, state: &IndexMap<String, Vec<u8>>) -> PyResult<()> {
394 if let Some(py_self) = self.python_instance()? {
395 Python::attach(|py| -> PyResult<()> {
396 let py_state = state_to_pydict(py, state)?;
397 py_self.call_method1(py, "on_load", (py_state,))?;
398 Ok(())
399 })?;
400 }
401 Ok(())
402 }
403
404 fn dispatch_on_time_event(&mut self, event: TimeEvent) -> PyResult<()> {
405 if let Some(py_self) = self.python_instance()? {
406 Python::attach(|py| {
407 py_self.call_method1(py, "on_time_event", (event.into_py_any(py)?,))
408 })?;
409 }
410 Ok(())
411 }
412
413 fn dispatch_on_data(&mut self, data: Py<PyAny>) -> PyResult<()> {
414 if let Some(py_self) = self.python_instance()? {
415 Python::attach(|py| py_self.call_method1(py, "on_data", (data,)))?;
416 }
417 Ok(())
418 }
419
420 fn dispatch_on_signal(&mut self, signal: &Signal) -> PyResult<()> {
421 if let Some(py_self) = self.python_instance()? {
422 Python::attach(|py| {
423 py_self.call_method1(py, "on_signal", (signal.clone().into_py_any(py)?,))
424 })?;
425 }
426 Ok(())
427 }
428
429 fn dispatch_on_queue_state(&mut self, event: &QueueStateChanged) -> PyResult<()> {
430 if let Some(py_self) = self.python_instance()? {
431 Python::attach(|py| {
432 py_self.call_method1(py, "on_queue_state", (event.clone().into_py_any(py)?,))
433 })?;
434 }
435 Ok(())
436 }
437
438 fn dispatch_on_socket_state(&mut self, event: &SocketStateChanged) -> PyResult<()> {
439 if let Some(py_self) = self.python_instance()? {
440 Python::attach(|py| {
441 py_self.call_method1(py, "on_socket_state", (event.clone().into_py_any(py)?,))
442 })?;
443 }
444 Ok(())
445 }
446
447 fn dispatch_on_instrument(&mut self, instrument: Py<PyAny>) -> PyResult<()> {
448 if let Some(py_self) = self.python_instance()? {
449 Python::attach(|py| py_self.call_method1(py, "on_instrument", (instrument,)))?;
450 }
451 Ok(())
452 }
453
454 fn dispatch_on_quote(&mut self, quote: QuoteTick) -> PyResult<()> {
455 if let Some(py_self) = self.python_instance()? {
456 Python::attach(|py| py_self.call_method1(py, "on_quote", (quote.into_py_any(py)?,)))?;
457 }
458 Ok(())
459 }
460
461 fn dispatch_on_trade(&mut self, trade: TradeTick) -> PyResult<()> {
462 if let Some(py_self) = self.python_instance()? {
463 Python::attach(|py| py_self.call_method1(py, "on_trade", (trade.into_py_any(py)?,)))?;
464 }
465 Ok(())
466 }
467
468 fn dispatch_on_bar(&mut self, bar: Bar) -> PyResult<()> {
469 if let Some(py_self) = self.python_instance()? {
470 Python::attach(|py| py_self.call_method1(py, "on_bar", (bar.into_py_any(py)?,)))?;
471 }
472 Ok(())
473 }
474
475 fn dispatch_on_book_deltas(&mut self, deltas: OrderBookDeltas) -> PyResult<()> {
476 if let Some(py_self) = self.python_instance()? {
477 Python::attach(|py| {
478 py_self.call_method1(py, "on_book_deltas", (deltas.into_py_any(py)?,))
479 })?;
480 }
481 Ok(())
482 }
483
484 fn dispatch_on_book_depth(&mut self, depth: &OrderBookDepth) -> PyResult<()> {
485 if let Some(py_self) = self.python_instance()? {
486 Python::attach(|py| {
487 py_self.call_method1(py, "on_book_depth", (depth.clone().into_py_any(py)?,))
488 })?;
489 }
490 Ok(())
491 }
492
493 fn dispatch_on_book(&mut self, book: &OrderBook) -> PyResult<()> {
494 if let Some(py_self) = self.python_instance()? {
495 Python::attach(|py| {
496 py_self.call_method1(py, "on_book", (book.clone().into_py_any(py)?,))
497 })?;
498 }
499 Ok(())
500 }
501
502 fn dispatch_on_mark_price(&mut self, mark_price: MarkPriceUpdate) -> PyResult<()> {
503 if let Some(py_self) = self.python_instance()? {
504 Python::attach(|py| {
505 py_self.call_method1(py, "on_mark_price", (mark_price.into_py_any(py)?,))
506 })?;
507 }
508 Ok(())
509 }
510
511 fn dispatch_on_index_price(&mut self, index_price: IndexPriceUpdate) -> PyResult<()> {
512 if let Some(py_self) = self.python_instance()? {
513 Python::attach(|py| {
514 py_self.call_method1(py, "on_index_price", (index_price.into_py_any(py)?,))
515 })?;
516 }
517 Ok(())
518 }
519
520 fn dispatch_on_funding_rate(&mut self, funding_rate: FundingRateUpdate) -> PyResult<()> {
521 if let Some(py_self) = self.python_instance()? {
522 Python::attach(|py| {
523 py_self.call_method1(py, "on_funding_rate", (funding_rate.into_py_any(py)?,))
524 })?;
525 }
526 Ok(())
527 }
528
529 fn dispatch_on_instrument_status(&mut self, data: InstrumentStatus) -> PyResult<()> {
530 if let Some(py_self) = self.python_instance()? {
531 Python::attach(|py| {
532 py_self.call_method1(py, "on_instrument_status", (data.into_py_any(py)?,))
533 })?;
534 }
535 Ok(())
536 }
537
538 fn dispatch_on_instrument_close(&mut self, update: InstrumentClose) -> PyResult<()> {
539 if let Some(py_self) = self.python_instance()? {
540 Python::attach(|py| {
541 py_self.call_method1(py, "on_instrument_close", (update.into_py_any(py)?,))
542 })?;
543 }
544 Ok(())
545 }
546
547 fn dispatch_on_option_greeks(&mut self, greeks: OptionGreeks) -> PyResult<()> {
548 if let Some(py_self) = self.python_instance()? {
549 Python::attach(|py| {
550 py_self.call_method1(py, "on_option_greeks", (greeks.into_py_any(py)?,))
551 })?;
552 }
553 Ok(())
554 }
555
556 fn dispatch_on_option_chain(&mut self, slice: OptionChainSlice) -> PyResult<()> {
557 if let Some(py_self) = self.python_instance()? {
558 Python::attach(|py| {
559 py_self.call_method1(py, "on_option_chain", (slice.into_py_any(py)?,))
560 })?;
561 }
562 Ok(())
563 }
564
565 fn dispatch_on_historical_data(&mut self, data: Py<PyAny>) -> PyResult<()> {
566 if let Some(py_self) = self.python_instance()? {
567 Python::attach(|py| py_self.call_method1(py, "on_historical_data", (data,)))?;
568 }
569 Ok(())
570 }
571
572 fn dispatch_on_historical_book_deltas(&mut self, deltas: Vec<OrderBookDelta>) -> PyResult<()> {
573 if let Some(py_self) = self.python_instance()? {
574 Python::attach(|py| {
575 let py_deltas = deltas
576 .into_iter()
577 .map(|delta| delta.into_py_any(py))
578 .collect::<PyResult<Vec<_>>>()?;
579 py_self.call_method1(py, "on_historical_book_deltas", (py_deltas,))
580 })?;
581 }
582 Ok(())
583 }
584
585 fn dispatch_on_historical_book_depth(&mut self, depths: Vec<OrderBookDepth>) -> PyResult<()> {
586 if let Some(py_self) = self.python_instance()? {
587 Python::attach(|py| {
588 let py_depths = depths
589 .into_iter()
590 .map(|depth| depth.into_py_any(py))
591 .collect::<PyResult<Vec<_>>>()?;
592 py_self.call_method1(py, "on_historical_book_depth", (py_depths,))
593 })?;
594 }
595 Ok(())
596 }
597
598 fn dispatch_on_historical_quotes(&mut self, quotes: Vec<QuoteTick>) -> PyResult<()> {
599 if let Some(py_self) = self.python_instance()? {
600 Python::attach(|py| {
601 let py_quotes = quotes
602 .into_iter()
603 .map(|q| q.into_py_any(py))
604 .collect::<PyResult<Vec<_>>>()?;
605 py_self.call_method1(py, "on_historical_quotes", (py_quotes,))
606 })?;
607 }
608 Ok(())
609 }
610
611 fn dispatch_on_historical_trades(&mut self, trades: Vec<TradeTick>) -> PyResult<()> {
612 if let Some(py_self) = self.python_instance()? {
613 Python::attach(|py| {
614 let py_trades = trades
615 .into_iter()
616 .map(|t| t.into_py_any(py))
617 .collect::<PyResult<Vec<_>>>()?;
618 py_self.call_method1(py, "on_historical_trades", (py_trades,))
619 })?;
620 }
621 Ok(())
622 }
623
624 fn dispatch_on_historical_funding_rates(
625 &mut self,
626 funding_rates: Vec<FundingRateUpdate>,
627 ) -> PyResult<()> {
628 if let Some(py_self) = self.python_instance()? {
629 Python::attach(|py| {
630 let py_rates = funding_rates
631 .into_iter()
632 .map(|r| r.into_py_any(py))
633 .collect::<PyResult<Vec<_>>>()?;
634 py_self.call_method1(py, "on_historical_funding_rates", (py_rates,))
635 })?;
636 }
637 Ok(())
638 }
639
640 fn dispatch_on_historical_bars(&mut self, bars: Vec<Bar>) -> PyResult<()> {
641 if let Some(py_self) = self.python_instance()? {
642 Python::attach(|py| {
643 let py_bars = bars
644 .into_iter()
645 .map(|b| b.into_py_any(py))
646 .collect::<PyResult<Vec<_>>>()?;
647 py_self.call_method1(py, "on_historical_bars", (py_bars,))
648 })?;
649 }
650 Ok(())
651 }
652
653 fn dispatch_on_historical_mark_prices(
654 &mut self,
655 mark_prices: Vec<MarkPriceUpdate>,
656 ) -> PyResult<()> {
657 if let Some(py_self) = self.python_instance()? {
658 Python::attach(|py| {
659 let py_prices = mark_prices
660 .into_iter()
661 .map(|p| p.into_py_any(py))
662 .collect::<PyResult<Vec<_>>>()?;
663 py_self.call_method1(py, "on_historical_mark_prices", (py_prices,))
664 })?;
665 }
666 Ok(())
667 }
668
669 fn dispatch_on_historical_index_prices(
670 &mut self,
671 index_prices: Vec<IndexPriceUpdate>,
672 ) -> PyResult<()> {
673 if let Some(py_self) = self.python_instance()? {
674 Python::attach(|py| {
675 let py_prices = index_prices
676 .into_iter()
677 .map(|p| p.into_py_any(py))
678 .collect::<PyResult<Vec<_>>>()?;
679 py_self.call_method1(py, "on_historical_index_prices", (py_prices,))
680 })?;
681 }
682 Ok(())
683 }
684
685 #[cfg(feature = "defi")]
686 fn dispatch_on_block(&mut self, block: Block) -> PyResult<()> {
687 if let Some(py_self) = self.python_instance()? {
688 Python::attach(|py| py_self.call_method1(py, "on_block", (block.into_py_any(py)?,)))?;
689 }
690 Ok(())
691 }
692
693 #[cfg(feature = "defi")]
694 fn dispatch_on_pool(&mut self, pool: Pool) -> PyResult<()> {
695 if let Some(py_self) = self.python_instance()? {
696 Python::attach(|py| py_self.call_method1(py, "on_pool", (pool.into_py_any(py)?,)))?;
697 }
698 Ok(())
699 }
700
701 #[cfg(feature = "defi")]
702 fn dispatch_on_pool_swap(&mut self, swap: PoolSwap) -> PyResult<()> {
703 if let Some(py_self) = self.python_instance()? {
704 Python::attach(|py| {
705 py_self.call_method1(py, "on_pool_swap", (swap.into_py_any(py)?,))
706 })?;
707 }
708 Ok(())
709 }
710
711 #[cfg(feature = "defi")]
712 fn dispatch_on_pool_liquidity_update(&mut self, update: PoolLiquidityUpdate) -> PyResult<()> {
713 if let Some(py_self) = self.python_instance()? {
714 Python::attach(|py| {
715 py_self.call_method1(py, "on_pool_liquidity_update", (update.into_py_any(py)?,))
716 })?;
717 }
718 Ok(())
719 }
720
721 #[cfg(feature = "defi")]
722 fn dispatch_on_pool_fee_collect(&mut self, collect: PoolFeeCollect) -> PyResult<()> {
723 if let Some(py_self) = self.python_instance()? {
724 Python::attach(|py| {
725 py_self.call_method1(py, "on_pool_fee_collect", (collect.into_py_any(py)?,))
726 })?;
727 }
728 Ok(())
729 }
730
731 #[cfg(feature = "defi")]
732 fn dispatch_on_pool_flash(&mut self, flash: PoolFlash) -> PyResult<()> {
733 if let Some(py_self) = self.python_instance()? {
734 Python::attach(|py| {
735 py_self.call_method1(py, "on_pool_flash", (flash.into_py_any(py)?,))
736 })?;
737 }
738 Ok(())
739 }
740
741 fn python_instance(&self) -> PyResult<Option<Py<PyAny>>> {
744 upgrade_py_weakref(self.py_self.as_ref(), &self.core.actor_id)
745 }
746}
747
748fn dict_to_params(
749 py: Python<'_>,
750 params: Option<Py<PyDict>>,
751) -> PyResult<Option<nautilus_core::Params>> {
752 match params {
753 Some(dict) => from_pydict(py, &dict),
754 None => Ok(None),
755 }
756}
757
758fn state_to_pydict(py: Python<'_>, state: &IndexMap<String, Vec<u8>>) -> PyResult<Py<PyDict>> {
759 let py_state = PyDict::new(py);
760 for (key, value) in state {
761 py_state.set_item(key, PyBytes::new(py, value))?;
762 }
763 Ok(py_state.unbind())
764}
765
766fn pydict_to_state(state: &Bound<'_, PyDict>) -> PyResult<IndexMap<String, Vec<u8>>> {
767 let mut rust_state = IndexMap::with_capacity(state.len());
768 for (key, value) in state.iter() {
769 rust_state.insert(key.extract()?, value.extract()?);
770 }
771 Ok(rust_state)
772}
773
774#[allow(non_camel_case_types)]
780#[pyo3::pyclass(
781 module = "nautilus_trader.common",
782 name = "DataActor",
783 unsendable,
784 subclass,
785 weakref
786)]
787#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")]
788pub struct PyDataActor {
789 inner: Rc<UnsafeCell<PyDataActorInner>>,
790}
791
792impl Debug for PyDataActor {
793 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
794 f.debug_struct(stringify!(PyDataActor))
795 .field("inner", &self.inner())
796 .finish()
797 }
798}
799
800impl PyDataActor {
801 #[inline]
808 #[allow(unsafe_code)]
809 pub(crate) fn inner(&self) -> &PyDataActorInner {
810 unsafe { &*self.inner.get() }
811 }
812
813 #[inline]
820 #[allow(unsafe_code, clippy::mut_from_ref)]
821 pub(crate) fn inner_mut(&self) -> &mut PyDataActorInner {
822 unsafe { &mut *self.inner.get() }
823 }
824}
825
826impl Deref for PyDataActor {
827 type Target = DataActorCore;
828
829 fn deref(&self) -> &Self::Target {
830 &self.inner().core
831 }
832}
833
834impl DerefMut for PyDataActor {
835 fn deref_mut(&mut self) -> &mut Self::Target {
836 &mut self.inner_mut().core
837 }
838}
839
840impl PyDataActor {
841 pub fn new(config: Option<DataActorConfig>) -> Self {
843 let config = config.unwrap_or_default();
844 let core = DataActorCore::new(config);
845 let clock = PyClock::new_test(); let logger = PyLogger::new(core.actor_id().as_str());
847
848 let inner = PyDataActorInner {
849 core,
850 py_self: None,
851 config: None,
852 clock,
853 logger,
854 };
855
856 Self {
857 inner: Rc::new(UnsafeCell::new(inner)),
858 }
859 }
860
861 #[must_use]
867 pub fn from_py_config(config: Option<Py<PyAny>>) -> Self {
868 let actor_config = config
869 .as_ref()
870 .and_then(|obj| Python::attach(|py| obj.extract::<DataActorConfig>(py).ok()));
871 let mut actor = Self::new(actor_config);
872 actor.set_config(config);
873 actor
874 }
875
876 pub fn set_python_instance(&mut self, py_obj: &Bound<'_, PyAny>) -> PyResult<()> {
890 self.inner_mut().py_self = Some(PyWeakrefReference::new(py_obj)?.unbind());
891 Ok(())
892 }
893
894 pub fn set_config(&mut self, config: Option<Py<PyAny>>) {
899 self.inner_mut().config = config;
900 }
901
902 pub fn set_actor_id(&mut self, actor_id: ActorId) {
909 let inner = self.inner_mut();
910 inner.core.config.actor_id = Some(actor_id);
911 inner.core.actor_id = actor_id;
912 inner.logger = PyLogger::new(actor_id.as_str());
913 }
914
915 pub fn set_log_events(&mut self, log_events: bool) {
917 self.inner_mut().core.config.log_events = log_events;
918 }
919
920 pub fn set_log_commands(&mut self, log_commands: bool) {
922 self.inner_mut().core.config.log_commands = log_commands;
923 }
924
925 pub fn mem_address(&self) -> String {
927 self.inner().core.mem_address()
928 }
929
930 pub fn is_registered(&self) -> bool {
932 self.inner().core.is_registered()
933 }
934
935 pub fn register(
941 &mut self,
942 trader_id: TraderId,
943 clock: Rc<RefCell<dyn Clock>>,
944 cache: Rc<RefCell<Cache>>,
945 ) -> anyhow::Result<()> {
946 let inner = self.inner_mut();
947 inner.core.register(trader_id, clock, cache)?;
948
949 inner.clock = PyClock::from_rc(inner.core.clock_rc());
950
951 let actor_id = inner.actor_id().inner();
953 let callback = TimeEventCallback::from(move |event: TimeEvent| {
954 if let Some(mut actor) = try_get_actor_unchecked::<PyDataActorInner>(&actor_id) {
955 if let Err(e) = actor.on_time_event(&event) {
956 log::error!("Python time event handler failed for actor {actor_id}: {e}");
957 }
958 } else {
959 log::error!("Actor {actor_id} not found for time event handling");
960 }
961 });
962
963 inner.clock.inner_mut().register_default_handler(callback);
964
965 inner.initialize()
966 }
967
968 pub fn register_in_global_registries(&self) -> PyResult<()> {
980 let inner = self.inner();
981 let component_id = inner.component_id();
982 let actor_id = Actor::id(inner);
983
984 let Some(wrapper) = inner.python_instance()? else {
985 return Err(to_pyruntime_err(format!(
986 "Cannot register actor {actor_id} without a Python wrapper, call `set_python_instance` first"
987 )));
988 };
989
990 let inner_ref: Rc<UnsafeCell<PyDataActorInner>> = self.inner.clone();
991
992 let component_trait_ref: Rc<UnsafeCell<dyn Component>> = inner_ref.clone();
993 with_component_registry(|registry| {
994 registry.insert(component_id.inner(), component_trait_ref);
995 });
996
997 let actor_trait_ref: Rc<UnsafeCell<dyn Actor>> = inner_ref;
998 with_actor_registry(|registry| registry.insert(actor_id, actor_trait_ref));
999
1000 retain_python_wrapper(component_id, wrapper, inner.core.message_bus());
1001
1002 Ok(())
1003 }
1004}
1005
1006pub fn register_python_exec_algorithm_endpoint(exec_algorithm_id: ExecAlgorithmId) {
1007 let actor_id = exec_algorithm_id.inner();
1008 let endpoint: Ustr = format!("{exec_algorithm_id}.execute").into();
1009 let handler = ShareableMessageHandler::from_typed(move |command: &TradingCommand| {
1010 if let Some(mut algo) = try_get_actor_unchecked::<PyDataActorInner>(&actor_id) {
1011 if let Err(e) = algo.execute_exec_algorithm_command(command) {
1012 log::error!("Error executing command on Python algorithm {actor_id}: {e}");
1013 }
1014 } else {
1015 log::error!("Python execution algorithm {actor_id} not found in registry");
1016 }
1017 });
1018 msgbus::register_any(endpoint.into(), handler);
1019}
1020
1021impl DataActor for PyDataActorInner {
1022 fn on_start(&mut self) -> anyhow::Result<()> {
1023 self.dispatch_on_start()
1024 .map_err(|e| anyhow::anyhow!("Python on_start failed:\n{}", format_exception(&e)))
1025 }
1026
1027 fn on_stop(&mut self) -> anyhow::Result<()> {
1028 self.dispatch_on_stop()
1029 .map_err(|e| anyhow::anyhow!("Python on_stop failed:\n{}", format_exception(&e)))
1030 }
1031
1032 fn on_resume(&mut self) -> anyhow::Result<()> {
1033 self.dispatch_on_resume()
1034 .map_err(|e| anyhow::anyhow!("Python on_resume failed:\n{}", format_exception(&e)))
1035 }
1036
1037 fn on_reset(&mut self) -> anyhow::Result<()> {
1038 self.dispatch_on_reset()
1039 .map_err(|e| anyhow::anyhow!("Python on_reset failed:\n{}", format_exception(&e)))
1040 }
1041
1042 fn on_dispose(&mut self) -> anyhow::Result<()> {
1043 self.dispatch_on_dispose()
1044 .map_err(|e| anyhow::anyhow!("Python on_dispose failed:\n{}", format_exception(&e)))
1045 }
1046
1047 fn on_degrade(&mut self) -> anyhow::Result<()> {
1048 self.dispatch_on_degrade()
1049 .map_err(|e| anyhow::anyhow!("Python on_degrade failed:\n{}", format_exception(&e)))
1050 }
1051
1052 fn on_fault(&mut self) -> anyhow::Result<()> {
1053 self.dispatch_on_fault()
1054 .map_err(|e| anyhow::anyhow!("Python on_fault failed:\n{}", format_exception(&e)))
1055 }
1056
1057 fn on_save(&self) -> anyhow::Result<IndexMap<String, Vec<u8>>> {
1058 self.dispatch_on_save()
1059 .map_err(|e| anyhow::anyhow!("Python on_save failed:\n{}", format_exception(&e)))
1060 }
1061
1062 fn on_load(&mut self, state: IndexMap<String, Vec<u8>>) -> anyhow::Result<()> {
1063 self.dispatch_on_load(&state)
1064 .map_err(|e| anyhow::anyhow!("Python on_load failed:\n{}", format_exception(&e)))
1065 }
1066
1067 fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()> {
1068 self.dispatch_on_time_event(event.clone())
1069 .map_err(|e| anyhow::anyhow!("Python on_time_event failed:\n{}", format_exception(&e)))
1070 }
1071
1072 #[allow(unused_variables)]
1073 fn on_data(&mut self, data: &CustomData) -> anyhow::Result<()> {
1074 Python::attach(|py| {
1075 let py_data: Py<PyAny> = Py::new(py, data.clone())?.into_any();
1076 self.dispatch_on_data(py_data)
1077 .map_err(|e| anyhow::anyhow!("Python on_data failed:\n{}", format_exception(&e)))
1078 })
1079 }
1080
1081 fn on_signal(&mut self, signal: &Signal) -> anyhow::Result<()> {
1082 self.dispatch_on_signal(signal)
1083 .map_err(|e| anyhow::anyhow!("Python on_signal failed:\n{}", format_exception(&e)))
1084 }
1085
1086 fn on_queue_state(&mut self, event: &QueueStateChanged) -> anyhow::Result<()> {
1087 self.dispatch_on_queue_state(event)
1088 .map_err(|e| anyhow::anyhow!("Python on_queue_state failed:\n{}", format_exception(&e)))
1089 }
1090
1091 fn on_socket_state(&mut self, event: &SocketStateChanged) -> anyhow::Result<()> {
1092 self.dispatch_on_socket_state(event).map_err(|e| {
1093 anyhow::anyhow!("Python on_socket_state failed:\n{}", format_exception(&e))
1094 })
1095 }
1096
1097 fn on_instrument(&mut self, instrument: &InstrumentAny) -> anyhow::Result<()> {
1098 Python::attach(|py| {
1099 let py_instrument = instrument_any_to_pyobject(py, instrument.clone())
1100 .map_err(|e| anyhow::anyhow!("Failed to convert InstrumentAny to Python: {e}"))?;
1101 self.dispatch_on_instrument(py_instrument).map_err(|e| {
1102 anyhow::anyhow!("Python on_instrument failed:\n{}", format_exception(&e))
1103 })
1104 })
1105 }
1106
1107 fn on_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
1108 self.dispatch_on_quote(*quote)
1109 .map_err(|e| anyhow::anyhow!("Python on_quote failed:\n{}", format_exception(&e)))
1110 }
1111
1112 fn on_trade(&mut self, tick: &TradeTick) -> anyhow::Result<()> {
1113 self.dispatch_on_trade(*tick)
1114 .map_err(|e| anyhow::anyhow!("Python on_trade failed:\n{}", format_exception(&e)))
1115 }
1116
1117 fn on_bar(&mut self, bar: &Bar) -> anyhow::Result<()> {
1118 self.dispatch_on_bar(*bar)
1119 .map_err(|e| anyhow::anyhow!("Python on_bar failed:\n{}", format_exception(&e)))
1120 }
1121
1122 fn on_book_deltas(&mut self, deltas: &OrderBookDeltas) -> anyhow::Result<()> {
1123 self.dispatch_on_book_deltas(deltas.clone())
1124 .map_err(|e| anyhow::anyhow!("Python on_book_deltas failed:\n{}", format_exception(&e)))
1125 }
1126
1127 fn on_book_depth(&mut self, depth: &OrderBookDepth) -> anyhow::Result<()> {
1128 self.dispatch_on_book_depth(depth)
1129 .map_err(|e| anyhow::anyhow!("Python on_book_depth failed:\n{}", format_exception(&e)))
1130 }
1131
1132 fn on_book(&mut self, order_book: &OrderBook) -> anyhow::Result<()> {
1133 self.dispatch_on_book(order_book)
1134 .map_err(|e| anyhow::anyhow!("Python on_book failed:\n{}", format_exception(&e)))
1135 }
1136
1137 fn on_mark_price(&mut self, mark_price: &MarkPriceUpdate) -> anyhow::Result<()> {
1138 self.dispatch_on_mark_price(*mark_price)
1139 .map_err(|e| anyhow::anyhow!("Python on_mark_price failed:\n{}", format_exception(&e)))
1140 }
1141
1142 fn on_index_price(&mut self, index_price: &IndexPriceUpdate) -> anyhow::Result<()> {
1143 self.dispatch_on_index_price(*index_price)
1144 .map_err(|e| anyhow::anyhow!("Python on_index_price failed:\n{}", format_exception(&e)))
1145 }
1146
1147 fn on_funding_rate(&mut self, funding_rate: &FundingRateUpdate) -> anyhow::Result<()> {
1148 self.dispatch_on_funding_rate(*funding_rate).map_err(|e| {
1149 anyhow::anyhow!("Python on_funding_rate failed:\n{}", format_exception(&e))
1150 })
1151 }
1152
1153 fn on_instrument_status(&mut self, data: &InstrumentStatus) -> anyhow::Result<()> {
1154 self.dispatch_on_instrument_status(*data).map_err(|e| {
1155 anyhow::anyhow!(
1156 "Python on_instrument_status failed:\n{}",
1157 format_exception(&e)
1158 )
1159 })
1160 }
1161
1162 fn on_instrument_close(&mut self, update: &InstrumentClose) -> anyhow::Result<()> {
1163 self.dispatch_on_instrument_close(*update).map_err(|e| {
1164 anyhow::anyhow!(
1165 "Python on_instrument_close failed:\n{}",
1166 format_exception(&e)
1167 )
1168 })
1169 }
1170
1171 fn on_option_greeks(&mut self, greeks: &OptionGreeks) -> anyhow::Result<()> {
1172 self.dispatch_on_option_greeks(*greeks).map_err(|e| {
1173 anyhow::anyhow!("Python on_option_greeks failed:\n{}", format_exception(&e))
1174 })
1175 }
1176
1177 fn on_option_chain(&mut self, slice: &OptionChainSlice) -> anyhow::Result<()> {
1178 self.dispatch_on_option_chain(slice.clone()).map_err(|e| {
1179 anyhow::anyhow!("Python on_option_chain failed:\n{}", format_exception(&e))
1180 })
1181 }
1182
1183 #[cfg(feature = "defi")]
1184 fn on_block(&mut self, block: &Block) -> anyhow::Result<()> {
1185 self.dispatch_on_block(block.clone())
1186 .map_err(|e| anyhow::anyhow!("Python on_block failed:\n{}", format_exception(&e)))
1187 }
1188
1189 #[cfg(feature = "defi")]
1190 fn on_pool(&mut self, pool: &Pool) -> anyhow::Result<()> {
1191 self.dispatch_on_pool(pool.clone())
1192 .map_err(|e| anyhow::anyhow!("Python on_pool failed:\n{}", format_exception(&e)))
1193 }
1194
1195 #[cfg(feature = "defi")]
1196 fn on_pool_swap(&mut self, swap: &PoolSwap) -> anyhow::Result<()> {
1197 self.dispatch_on_pool_swap(swap.clone())
1198 .map_err(|e| anyhow::anyhow!("Python on_pool_swap failed:\n{}", format_exception(&e)))
1199 }
1200
1201 #[cfg(feature = "defi")]
1202 fn on_pool_liquidity_update(&mut self, update: &PoolLiquidityUpdate) -> anyhow::Result<()> {
1203 self.dispatch_on_pool_liquidity_update(update.clone())
1204 .map_err(|e| {
1205 anyhow::anyhow!(
1206 "Python on_pool_liquidity_update failed:\n{}",
1207 format_exception(&e)
1208 )
1209 })
1210 }
1211
1212 #[cfg(feature = "defi")]
1213 fn on_pool_fee_collect(&mut self, collect: &PoolFeeCollect) -> anyhow::Result<()> {
1214 self.dispatch_on_pool_fee_collect(collect.clone())
1215 .map_err(|e| {
1216 anyhow::anyhow!(
1217 "Python on_pool_fee_collect failed:\n{}",
1218 format_exception(&e)
1219 )
1220 })
1221 }
1222
1223 #[cfg(feature = "defi")]
1224 fn on_pool_flash(&mut self, flash: &PoolFlash) -> anyhow::Result<()> {
1225 self.dispatch_on_pool_flash(flash.clone())
1226 .map_err(|e| anyhow::anyhow!("Python on_pool_flash failed:\n{}", format_exception(&e)))
1227 }
1228
1229 fn on_historical_data(&mut self, data: &dyn Any) -> anyhow::Result<()> {
1230 Python::attach(|py| {
1231 let py_data: Py<PyAny> = if let Some(custom_data) = data.downcast_ref::<CustomData>() {
1232 Py::new(py, custom_data.clone())?.into_any()
1233 } else if let Some(custom_data) = data.downcast_ref::<Vec<CustomData>>() {
1234 custom_data.clone().into_py_any(py)?
1235 } else {
1236 anyhow::bail!("Failed to convert historical data to Python: unsupported type");
1237 };
1238
1239 self.dispatch_on_historical_data(py_data).map_err(|e| {
1240 anyhow::anyhow!(
1241 "Python on_historical_data failed:\n{}",
1242 format_exception(&e)
1243 )
1244 })
1245 })
1246 }
1247
1248 fn on_historical_book_deltas(&mut self, deltas: &[OrderBookDelta]) -> anyhow::Result<()> {
1249 self.dispatch_on_historical_book_deltas(deltas.to_vec())
1250 .map_err(|e| {
1251 anyhow::anyhow!(
1252 "Python on_historical_book_deltas failed:\n{}",
1253 format_exception(&e)
1254 )
1255 })
1256 }
1257
1258 fn on_historical_book_depth(&mut self, depths: &[OrderBookDepth]) -> anyhow::Result<()> {
1259 self.dispatch_on_historical_book_depth(depths.to_vec())
1260 .map_err(|e| {
1261 anyhow::anyhow!(
1262 "Python on_historical_book_depth failed:\n{}",
1263 format_exception(&e)
1264 )
1265 })
1266 }
1267
1268 fn on_historical_quotes(&mut self, quotes: &[QuoteTick]) -> anyhow::Result<()> {
1269 self.dispatch_on_historical_quotes(quotes.to_vec())
1270 .map_err(|e| {
1271 anyhow::anyhow!(
1272 "Python on_historical_quotes failed:\n{}",
1273 format_exception(&e)
1274 )
1275 })
1276 }
1277
1278 fn on_historical_trades(&mut self, trades: &[TradeTick]) -> anyhow::Result<()> {
1279 self.dispatch_on_historical_trades(trades.to_vec())
1280 .map_err(|e| {
1281 anyhow::anyhow!(
1282 "Python on_historical_trades failed:\n{}",
1283 format_exception(&e)
1284 )
1285 })
1286 }
1287
1288 fn on_historical_funding_rates(
1289 &mut self,
1290 funding_rates: &[FundingRateUpdate],
1291 ) -> anyhow::Result<()> {
1292 self.dispatch_on_historical_funding_rates(funding_rates.to_vec())
1293 .map_err(|e| {
1294 anyhow::anyhow!(
1295 "Python on_historical_funding_rates failed:\n{}",
1296 format_exception(&e)
1297 )
1298 })
1299 }
1300
1301 fn on_historical_bars(&mut self, bars: &[Bar]) -> anyhow::Result<()> {
1302 self.dispatch_on_historical_bars(bars.to_vec())
1303 .map_err(|e| {
1304 anyhow::anyhow!(
1305 "Python on_historical_bars failed:\n{}",
1306 format_exception(&e)
1307 )
1308 })
1309 }
1310
1311 fn on_historical_mark_prices(&mut self, mark_prices: &[MarkPriceUpdate]) -> anyhow::Result<()> {
1312 self.dispatch_on_historical_mark_prices(mark_prices.to_vec())
1313 .map_err(|e| {
1314 anyhow::anyhow!(
1315 "Python on_historical_mark_prices failed:\n{}",
1316 format_exception(&e)
1317 )
1318 })
1319 }
1320
1321 fn on_historical_index_prices(
1322 &mut self,
1323 index_prices: &[IndexPriceUpdate],
1324 ) -> anyhow::Result<()> {
1325 self.dispatch_on_historical_index_prices(index_prices.to_vec())
1326 .map_err(|e| {
1327 anyhow::anyhow!(
1328 "Python on_historical_index_prices failed:\n{}",
1329 format_exception(&e)
1330 )
1331 })
1332 }
1333}
1334
1335#[pymethods]
1336#[pyo3_stub_gen::derive::gen_stub_pymethods]
1337impl PyDataActor {
1338 #[new]
1350 #[pyo3(signature = (config=None))]
1351 fn py_new(config: Option<Py<PyAny>>) -> Self {
1352 Self::from_py_config(config)
1353 }
1354
1355 #[pyo3(signature = (config=None))]
1356 fn __init__(slf: &Bound<'_, Self>, config: Option<Py<PyAny>>) -> PyResult<()> {
1357 {
1358 let mut borrowed = slf.borrow_mut();
1359 borrowed.set_python_instance(slf.as_any())?;
1360 if config.is_some() {
1362 borrowed.set_config(config);
1363 }
1364 }
1365
1366 if !has_configured_actor_id(slf) {
1367 let py_type = slf.get_type();
1368 let type_name = py_type.name()?;
1369 let actor_id = ActorId::new_checked(type_name.to_str()?).map_err(to_pyvalue_err)?;
1370 slf.borrow_mut().set_actor_id(actor_id);
1371 }
1372
1373 Ok(())
1374 }
1375
1376 #[getter]
1377 #[pyo3(name = "clock")]
1378 fn py_clock(&self) -> PyResult<PyClock> {
1379 let inner = self.inner();
1380 if inner.core.is_registered() {
1381 Ok(inner.clock.clone())
1382 } else {
1383 Err(to_pyruntime_err(
1384 "Actor must be registered with a trader before accessing clock",
1385 ))
1386 }
1387 }
1388
1389 #[getter]
1390 #[pyo3(name = "cache")]
1391 fn py_cache(&self) -> PyResult<PyCache> {
1392 let inner = self.inner();
1393 if inner.core.is_registered() {
1394 Ok(PyCache::from_rc(inner.core.cache_rc()))
1395 } else {
1396 Err(to_pyruntime_err(
1397 "Actor must be registered with a trader before accessing cache",
1398 ))
1399 }
1400 }
1401
1402 #[getter]
1403 #[pyo3(name = "log")]
1404 fn py_log(&self) -> PyLogger {
1405 self.inner().logger.clone()
1406 }
1407
1408 #[getter]
1409 #[pyo3(name = "actor_id")]
1410 fn py_actor_id(&self) -> ActorId {
1411 self.inner().core.actor_id
1412 }
1413
1414 #[getter]
1415 #[pyo3(name = "config")]
1416 fn py_config(&self, py: Python<'_>) -> Option<Py<PyAny>> {
1417 self.inner()
1418 .config
1419 .as_ref()
1420 .map(|config| config.clone_ref(py))
1421 }
1422
1423 #[getter]
1424 #[pyo3(name = "trader_id")]
1425 fn py_trader_id(&self) -> Option<TraderId> {
1426 self.inner().core.trader_id()
1427 }
1428
1429 #[pyo3(name = "state")]
1430 fn py_state(&self) -> ComponentState {
1431 Component::state(self.inner())
1432 }
1433
1434 #[pyo3(name = "is_ready")]
1435 fn py_is_ready(&self) -> bool {
1436 Component::is_ready(self.inner())
1437 }
1438
1439 #[pyo3(name = "is_running")]
1440 fn py_is_running(&self) -> bool {
1441 Component::is_running(self.inner())
1442 }
1443
1444 #[pyo3(name = "is_stopped")]
1445 fn py_is_stopped(&self) -> bool {
1446 Component::is_stopped(self.inner())
1447 }
1448
1449 #[pyo3(name = "is_degraded")]
1450 fn py_is_degraded(&self) -> bool {
1451 Component::is_degraded(self.inner())
1452 }
1453
1454 #[pyo3(name = "is_faulted")]
1455 fn py_is_faulted(&self) -> bool {
1456 Component::is_faulted(self.inner())
1457 }
1458
1459 #[pyo3(name = "is_disposed")]
1460 fn py_is_disposed(&self) -> bool {
1461 Component::is_disposed(self.inner())
1462 }
1463
1464 #[pyo3(name = "start")]
1465 fn py_start(&mut self) -> PyResult<()> {
1466 Component::start(self.inner_mut()).map_err(to_pyruntime_err)
1467 }
1468
1469 #[pyo3(name = "stop")]
1470 fn py_stop(&mut self) -> PyResult<()> {
1471 Component::stop(self.inner_mut()).map_err(to_pyruntime_err)
1472 }
1473
1474 #[pyo3(name = "save")]
1475 fn py_save(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
1476 let state = DataActor::on_save(self.inner()).map_err(to_pyruntime_err)?;
1477 state_to_pydict(py, &state)
1478 }
1479
1480 #[pyo3(name = "load")]
1481 fn py_load(&mut self, state: &Bound<'_, PyDict>) -> PyResult<()> {
1482 let state = pydict_to_state(state)?;
1483 DataActor::on_load(self.inner_mut(), state).map_err(to_pyruntime_err)
1484 }
1485
1486 #[pyo3(name = "resume")]
1487 fn py_resume(&mut self) -> PyResult<()> {
1488 Component::resume(self.inner_mut()).map_err(to_pyruntime_err)
1489 }
1490
1491 #[pyo3(name = "reset")]
1492 fn py_reset(&mut self) -> PyResult<()> {
1493 Component::reset(self.inner_mut()).map_err(to_pyruntime_err)
1494 }
1495
1496 #[pyo3(name = "dispose")]
1497 fn py_dispose(&mut self) -> PyResult<()> {
1498 Component::dispose(self.inner_mut()).map_err(to_pyruntime_err)
1499 }
1500
1501 #[pyo3(name = "degrade")]
1502 fn py_degrade(&mut self) -> PyResult<()> {
1503 Component::degrade(self.inner_mut()).map_err(to_pyruntime_err)
1504 }
1505
1506 #[pyo3(name = "fault")]
1507 fn py_fault(&mut self) -> PyResult<()> {
1508 Component::fault(self.inner_mut()).map_err(to_pyruntime_err)
1509 }
1510
1511 #[pyo3(name = "shutdown_system")]
1512 #[pyo3(signature = (reason=None))]
1513 fn py_shutdown_system(&self, reason: Option<String>) -> PyResult<()> {
1514 if !self.inner().core.is_registered() {
1515 return Err(to_pyruntime_err(
1516 "Actor must be registered with a trader before shutting down the system",
1517 ));
1518 }
1519
1520 self.inner().shutdown_system(reason);
1521 Ok(())
1522 }
1523
1524 #[pyo3(name = "publish_data")]
1525 fn py_publish_data(&self, data_type: &DataType, data: &CustomData) -> PyResult<()> {
1526 self.ensure_registered_for_data()?;
1527 self.inner().publish_data(data_type, data);
1528 Ok(())
1529 }
1530
1531 #[pyo3(name = "publish_signal")]
1532 #[pyo3(signature = (name, value, ts_event=0))]
1533 #[expect(
1534 clippy::needless_pass_by_value,
1535 reason = "PyO3 accepts an owned PyAny handle for Python signal values"
1536 )]
1537 fn py_publish_signal(
1538 &self,
1539 py: Python<'_>,
1540 name: &str,
1541 value: Py<PyAny>,
1542 ts_event: u64,
1543 ) -> PyResult<()> {
1544 self.ensure_registered_for_data()?;
1545 let value_str: String = value.bind(py).str()?.extract()?;
1546 self.inner()
1547 .publish_signal(name, value_str, UnixNanos::from(ts_event));
1548 Ok(())
1549 }
1550
1551 #[pyo3(name = "add_synthetic")]
1552 fn py_add_synthetic(&self, synthetic: SyntheticInstrument) -> PyResult<()> {
1553 self.ensure_registered_for_data()?;
1554 self.inner()
1555 .add_synthetic(synthetic)
1556 .map_err(to_pyvalue_err)
1557 }
1558
1559 #[pyo3(name = "update_synthetic")]
1560 fn py_update_synthetic(&self, synthetic: SyntheticInstrument) -> PyResult<()> {
1561 self.ensure_registered_for_data()?;
1562 self.inner()
1563 .update_synthetic(synthetic)
1564 .map_err(to_pyvalue_err)
1565 }
1566
1567 #[getter]
1568 #[pyo3(name = "registered_indicators")]
1569 fn py_registered_indicators(&self, py: Python<'_>) -> PyResult<Py<PyList>> {
1570 registered_python_indicators(py, self.inner().core.registered_indicators())
1571 }
1572
1573 #[pyo3(name = "indicators_initialized")]
1574 fn py_indicators_initialized(&self, _py: Python<'_>) -> PyResult<bool> {
1575 self.inner()
1576 .core
1577 .indicators_initialized()
1578 .map_err(to_pyruntime_err)
1579 }
1580
1581 #[pyo3(name = "register_indicator_for_quote_ticks")]
1582 fn py_register_indicator_for_quote_ticks(
1583 &mut self,
1584 py: Python<'_>,
1585 instrument_id: InstrumentId,
1586 indicator: Py<PyAny>,
1587 ) {
1588 let indicator = wrap_python_indicator(py, indicator);
1589 self.inner_mut()
1590 .core
1591 .register_indicator_for_quote_ticks(instrument_id, indicator);
1592 }
1593
1594 #[pyo3(name = "register_indicator_for_trade_ticks")]
1595 fn py_register_indicator_for_trade_ticks(
1596 &mut self,
1597 py: Python<'_>,
1598 instrument_id: InstrumentId,
1599 indicator: Py<PyAny>,
1600 ) {
1601 let indicator = wrap_python_indicator(py, indicator);
1602 self.inner_mut()
1603 .core
1604 .register_indicator_for_trade_ticks(instrument_id, indicator);
1605 }
1606
1607 #[pyo3(name = "register_indicator_for_bars")]
1608 fn py_register_indicator_for_bars(
1609 &mut self,
1610 py: Python<'_>,
1611 bar_type: BarType,
1612 indicator: Py<PyAny>,
1613 ) {
1614 let indicator = wrap_python_indicator(py, indicator);
1615 self.inner_mut()
1616 .core
1617 .register_indicator_for_bars(bar_type, indicator);
1618 }
1619
1620 #[pyo3(name = "on_start")]
1621 fn py_on_start(_slf: &Bound<'_, Self>) {}
1622
1623 #[pyo3(name = "on_stop")]
1624 fn py_on_stop(_slf: &Bound<'_, Self>) {}
1625
1626 #[pyo3(name = "on_resume")]
1627 fn py_on_resume(_slf: &Bound<'_, Self>) {}
1628
1629 #[pyo3(name = "on_reset")]
1630 fn py_on_reset(_slf: &Bound<'_, Self>) {}
1631
1632 #[pyo3(name = "on_dispose")]
1633 fn py_on_dispose(_slf: &Bound<'_, Self>) {}
1634
1635 #[pyo3(name = "on_degrade")]
1636 fn py_on_degrade(_slf: &Bound<'_, Self>) {}
1637
1638 #[pyo3(name = "on_fault")]
1639 fn py_on_fault(_slf: &Bound<'_, Self>) {}
1640
1641 #[pyo3(name = "on_save")]
1642 fn py_on_save(_slf: &Bound<'_, Self>, py: Python<'_>) -> Py<PyDict> {
1643 PyDict::new(py).unbind()
1644 }
1645
1646 #[allow(unused_variables)]
1647 #[pyo3(name = "on_load")]
1648 fn py_on_load(_slf: &Bound<'_, Self>, state: &Bound<'_, PyDict>) {}
1649
1650 #[allow(unused_variables, clippy::needless_pass_by_value)]
1651 #[pyo3(name = "on_time_event")]
1652 fn py_on_time_event(_slf: &Bound<'_, Self>, event: TimeEvent) {}
1653
1654 #[allow(unused_variables, clippy::needless_pass_by_value)]
1655 #[pyo3(name = "on_data")]
1656 fn py_on_data(_slf: &Bound<'_, Self>, data: Py<PyAny>) {}
1657
1658 #[allow(unused_variables)]
1659 #[pyo3(name = "on_signal")]
1660 fn py_on_signal(_slf: &Bound<'_, Self>, signal: &Signal) {}
1661
1662 #[allow(unused_variables, clippy::needless_pass_by_value)]
1663 #[pyo3(name = "on_queue_state")]
1664 fn py_on_queue_state(_slf: &Bound<'_, Self>, event: QueueStateChanged) {}
1665
1666 #[allow(unused_variables, clippy::needless_pass_by_value)]
1667 #[pyo3(name = "on_socket_state")]
1668 fn py_on_socket_state(_slf: &Bound<'_, Self>, event: SocketStateChanged) {}
1669
1670 #[allow(unused_variables, clippy::needless_pass_by_value)]
1671 #[pyo3(name = "on_instrument")]
1672 fn py_on_instrument(_slf: &Bound<'_, Self>, instrument: Py<PyAny>) {}
1673
1674 #[allow(unused_variables)]
1675 #[pyo3(name = "on_quote")]
1676 fn py_on_quote(_slf: &Bound<'_, Self>, quote: QuoteTick) {}
1677
1678 #[allow(unused_variables)]
1679 #[pyo3(name = "on_trade")]
1680 fn py_on_trade(_slf: &Bound<'_, Self>, trade: TradeTick) {}
1681
1682 #[allow(unused_variables)]
1683 #[pyo3(name = "on_bar")]
1684 fn py_on_bar(_slf: &Bound<'_, Self>, bar: Bar) {}
1685
1686 #[allow(unused_variables, clippy::needless_pass_by_value)]
1687 #[pyo3(name = "on_book_deltas")]
1688 fn py_on_book_deltas(_slf: &Bound<'_, Self>, deltas: OrderBookDeltas) {}
1689
1690 #[allow(unused_variables)]
1691 #[pyo3(name = "on_book_depth")]
1692 fn py_on_book_depth(_slf: &Bound<'_, Self>, depth: &OrderBookDepth) {}
1693
1694 #[allow(unused_variables)]
1695 #[pyo3(name = "on_book")]
1696 fn py_on_book(_slf: &Bound<'_, Self>, book: &OrderBook) {}
1697
1698 #[allow(unused_variables)]
1699 #[pyo3(name = "on_mark_price")]
1700 fn py_on_mark_price(_slf: &Bound<'_, Self>, mark_price: MarkPriceUpdate) {}
1701
1702 #[allow(unused_variables)]
1703 #[pyo3(name = "on_index_price")]
1704 fn py_on_index_price(_slf: &Bound<'_, Self>, index_price: IndexPriceUpdate) {}
1705
1706 #[allow(unused_variables)]
1707 #[pyo3(name = "on_funding_rate")]
1708 fn py_on_funding_rate(_slf: &Bound<'_, Self>, funding_rate: FundingRateUpdate) {}
1709
1710 #[allow(unused_variables)]
1711 #[pyo3(name = "on_instrument_status")]
1712 fn py_on_instrument_status(_slf: &Bound<'_, Self>, status: InstrumentStatus) {}
1713
1714 #[allow(unused_variables)]
1715 #[pyo3(name = "on_instrument_close")]
1716 fn py_on_instrument_close(_slf: &Bound<'_, Self>, close: InstrumentClose) {}
1717
1718 #[allow(unused_variables)]
1719 #[pyo3(name = "on_option_greeks")]
1720 fn py_on_option_greeks(_slf: &Bound<'_, Self>, greeks: OptionGreeks) {}
1721
1722 #[allow(unused_variables, clippy::needless_pass_by_value)]
1723 #[pyo3(name = "on_option_chain")]
1724 fn py_on_option_chain(_slf: &Bound<'_, Self>, slice: OptionChainSlice) {}
1725
1726 #[pyo3(name = "subscribe_data")]
1727 #[pyo3(signature = (data_type, client_id=None, params=None))]
1728 fn py_subscribe_data(
1729 &mut self,
1730 py: Python<'_>,
1731 data_type: DataType,
1732 client_id: Option<ClientId>,
1733 params: Option<Py<PyDict>>,
1734 ) -> PyResult<()> {
1735 self.ensure_registered()?;
1736 let params = dict_to_params(py, params)?;
1737 DataActor::subscribe_data(self.inner_mut(), data_type, client_id, params);
1738 Ok(())
1739 }
1740
1741 #[pyo3(name = "subscribe_signal")]
1742 #[pyo3(signature = (name="", priority=None))]
1743 fn py_subscribe_signal(
1744 slf: &Bound<'_, Self>,
1745 name: &str,
1746 priority: Option<u32>,
1747 ) -> PyResult<()> {
1748 let actor = borrow_actor_mut(slf, "subscribe_signal")?;
1749 actor.ensure_registered()?;
1750 DataActor::subscribe_signal(actor.inner_mut(), name, priority);
1751 Ok(())
1752 }
1753
1754 #[pyo3(name = "subscribe_queue_state")]
1755 #[pyo3(signature = (channel=None, priority=None))]
1756 fn py_subscribe_queue_state(
1757 &mut self,
1758 channel: Option<SystemChannel>,
1759 priority: Option<u32>,
1760 ) -> PyResult<()> {
1761 self.ensure_registered()?;
1762 DataActor::subscribe_queue_state(self.inner_mut(), channel, priority);
1763 Ok(())
1764 }
1765
1766 #[pyo3(name = "subscribe_socket_state")]
1767 #[pyo3(signature = (client_id=None, endpoint=None, priority=None))]
1768 fn py_subscribe_socket_state(
1769 &mut self,
1770 client_id: Option<ClientId>,
1771 endpoint: Option<&str>,
1772 priority: Option<u32>,
1773 ) -> PyResult<()> {
1774 self.ensure_registered()?;
1775 DataActor::subscribe_socket_state(self.inner_mut(), client_id, endpoint, priority);
1776 Ok(())
1777 }
1778
1779 #[pyo3(name = "subscribe_instruments")]
1780 #[pyo3(signature = (venue, client_id=None, params=None))]
1781 fn py_subscribe_instruments(
1782 &mut self,
1783 py: Python<'_>,
1784 venue: Venue,
1785 client_id: Option<ClientId>,
1786 params: Option<Py<PyDict>>,
1787 ) -> PyResult<()> {
1788 self.ensure_registered()?;
1789 let params = dict_to_params(py, params)?;
1790 DataActor::subscribe_instruments(self.inner_mut(), venue, client_id, params);
1791 Ok(())
1792 }
1793
1794 #[pyo3(name = "subscribe_instrument")]
1795 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1796 fn py_subscribe_instrument(
1797 &mut self,
1798 py: Python<'_>,
1799 instrument_id: InstrumentId,
1800 client_id: Option<ClientId>,
1801 params: Option<Py<PyDict>>,
1802 ) -> PyResult<()> {
1803 self.ensure_registered()?;
1804 let params = dict_to_params(py, params)?;
1805 DataActor::subscribe_instrument(self.inner_mut(), instrument_id, client_id, params);
1806 Ok(())
1807 }
1808
1809 #[pyo3(name = "subscribe_book_deltas")]
1810 #[pyo3(signature = (instrument_id, book_type, depth=None, client_id=None, managed=false, params=None))]
1811 #[expect(clippy::too_many_arguments)]
1812 fn py_subscribe_book_deltas(
1813 &mut self,
1814 py: Python<'_>,
1815 instrument_id: InstrumentId,
1816 book_type: BookType,
1817 depth: Option<usize>,
1818 client_id: Option<ClientId>,
1819 managed: bool,
1820 params: Option<Py<PyDict>>,
1821 ) -> PyResult<()> {
1822 self.ensure_registered()?;
1823 let params = dict_to_params(py, params)?;
1824 let depth = depth.and_then(NonZeroUsize::new);
1825 DataActor::subscribe_book_deltas(
1826 self.inner_mut(),
1827 instrument_id,
1828 book_type,
1829 depth,
1830 client_id,
1831 managed,
1832 params,
1833 );
1834 Ok(())
1835 }
1836
1837 #[expect(clippy::too_many_arguments)]
1838 #[pyo3(name = "subscribe_book_depth")]
1839 #[pyo3(signature = (instrument_id, book_type, depth=None, client_id=None, managed=false, params=None))]
1840 fn py_subscribe_book_depth(
1841 &mut self,
1842 py: Python<'_>,
1843 instrument_id: InstrumentId,
1844 book_type: BookType,
1845 depth: Option<usize>,
1846 client_id: Option<ClientId>,
1847 managed: bool,
1848 params: Option<Py<PyDict>>,
1849 ) -> PyResult<()> {
1850 self.ensure_registered()?;
1851
1852 let depth = depth
1853 .map(|value| {
1854 NonZeroUsize::new(value).ok_or_else(|| to_pyvalue_err("depth must be positive"))
1855 })
1856 .transpose()?;
1857
1858 let params = dict_to_params(py, params)?;
1859 DataActor::subscribe_book_depth(
1860 self.inner_mut(),
1861 instrument_id,
1862 book_type,
1863 depth,
1864 client_id,
1865 managed,
1866 params,
1867 );
1868 Ok(())
1869 }
1870
1871 #[pyo3(name = "subscribe_book_at_interval")]
1872 #[pyo3(signature = (instrument_id, book_type, interval_ms, depth=None, client_id=None, params=None))]
1873 #[expect(clippy::too_many_arguments)]
1874 fn py_subscribe_book_at_interval(
1875 &mut self,
1876 py: Python<'_>,
1877 instrument_id: InstrumentId,
1878 book_type: BookType,
1879 interval_ms: usize,
1880 depth: Option<usize>,
1881 client_id: Option<ClientId>,
1882 params: Option<Py<PyDict>>,
1883 ) -> PyResult<()> {
1884 let interval_ms = NonZeroUsize::new(interval_ms)
1885 .ok_or_else(|| to_pyvalue_err("interval_ms must be > 0"))?;
1886
1887 self.ensure_registered()?;
1888 let params = dict_to_params(py, params)?;
1889 let depth = depth.and_then(NonZeroUsize::new);
1890 DataActor::subscribe_book_at_interval(
1891 self.inner_mut(),
1892 instrument_id,
1893 book_type,
1894 depth,
1895 interval_ms,
1896 client_id,
1897 params,
1898 );
1899 Ok(())
1900 }
1901
1902 #[pyo3(name = "subscribe_quotes")]
1903 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1904 fn py_subscribe_quotes(
1905 &mut self,
1906 py: Python<'_>,
1907 instrument_id: InstrumentId,
1908 client_id: Option<ClientId>,
1909 params: Option<Py<PyDict>>,
1910 ) -> PyResult<()> {
1911 self.ensure_registered()?;
1912 let params = dict_to_params(py, params)?;
1913 DataActor::subscribe_quotes(self.inner_mut(), instrument_id, client_id, params);
1914 Ok(())
1915 }
1916
1917 #[pyo3(name = "subscribe_trades")]
1918 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1919 fn py_subscribe_trades(
1920 &mut self,
1921 py: Python<'_>,
1922 instrument_id: InstrumentId,
1923 client_id: Option<ClientId>,
1924 params: Option<Py<PyDict>>,
1925 ) -> PyResult<()> {
1926 self.ensure_registered()?;
1927 let params = dict_to_params(py, params)?;
1928 DataActor::subscribe_trades(self.inner_mut(), instrument_id, client_id, params);
1929 Ok(())
1930 }
1931
1932 #[pyo3(name = "subscribe_bars")]
1933 #[pyo3(signature = (bar_type, client_id=None, params=None))]
1934 fn py_subscribe_bars(
1935 &mut self,
1936 py: Python<'_>,
1937 bar_type: BarType,
1938 client_id: Option<ClientId>,
1939 params: Option<Py<PyDict>>,
1940 ) -> PyResult<()> {
1941 self.ensure_registered()?;
1942 let params = dict_to_params(py, params)?;
1943 DataActor::subscribe_bars(self.inner_mut(), bar_type, client_id, params);
1944 Ok(())
1945 }
1946
1947 #[pyo3(name = "subscribe_mark_prices")]
1948 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1949 fn py_subscribe_mark_prices(
1950 &mut self,
1951 py: Python<'_>,
1952 instrument_id: InstrumentId,
1953 client_id: Option<ClientId>,
1954 params: Option<Py<PyDict>>,
1955 ) -> PyResult<()> {
1956 self.ensure_registered()?;
1957 let params = dict_to_params(py, params)?;
1958 DataActor::subscribe_mark_prices(self.inner_mut(), instrument_id, client_id, params);
1959 Ok(())
1960 }
1961
1962 #[pyo3(name = "subscribe_index_prices")]
1963 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1964 fn py_subscribe_index_prices(
1965 &mut self,
1966 py: Python<'_>,
1967 instrument_id: InstrumentId,
1968 client_id: Option<ClientId>,
1969 params: Option<Py<PyDict>>,
1970 ) -> PyResult<()> {
1971 self.ensure_registered()?;
1972 let params = dict_to_params(py, params)?;
1973 DataActor::subscribe_index_prices(self.inner_mut(), instrument_id, client_id, params);
1974 Ok(())
1975 }
1976
1977 #[pyo3(name = "subscribe_funding_rates")]
1978 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1979 fn py_subscribe_funding_rates(
1980 &mut self,
1981 py: Python<'_>,
1982 instrument_id: InstrumentId,
1983 client_id: Option<ClientId>,
1984 params: Option<Py<PyDict>>,
1985 ) -> PyResult<()> {
1986 self.ensure_registered()?;
1987 let params = dict_to_params(py, params)?;
1988 DataActor::subscribe_funding_rates(self.inner_mut(), instrument_id, client_id, params);
1989 Ok(())
1990 }
1991
1992 #[pyo3(name = "subscribe_option_greeks")]
1993 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1994 fn py_subscribe_option_greeks(
1995 &mut self,
1996 py: Python<'_>,
1997 instrument_id: InstrumentId,
1998 client_id: Option<ClientId>,
1999 params: Option<Py<PyDict>>,
2000 ) -> PyResult<()> {
2001 self.ensure_registered()?;
2002 let params = dict_to_params(py, params)?;
2003 DataActor::subscribe_option_greeks(self.inner_mut(), instrument_id, client_id, params);
2004 Ok(())
2005 }
2006
2007 #[pyo3(name = "subscribe_instrument_status")]
2008 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2009 fn py_subscribe_instrument_status(
2010 &mut self,
2011 py: Python<'_>,
2012 instrument_id: InstrumentId,
2013 client_id: Option<ClientId>,
2014 params: Option<Py<PyDict>>,
2015 ) -> PyResult<()> {
2016 self.ensure_registered()?;
2017 let params = dict_to_params(py, params)?;
2018 DataActor::subscribe_instrument_status(self.inner_mut(), instrument_id, client_id, params);
2019 Ok(())
2020 }
2021
2022 #[pyo3(name = "subscribe_instrument_close")]
2023 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2024 fn py_subscribe_instrument_close(
2025 &mut self,
2026 py: Python<'_>,
2027 instrument_id: InstrumentId,
2028 client_id: Option<ClientId>,
2029 params: Option<Py<PyDict>>,
2030 ) -> PyResult<()> {
2031 self.ensure_registered()?;
2032 let params = dict_to_params(py, params)?;
2033 DataActor::subscribe_instrument_close(self.inner_mut(), instrument_id, client_id, params);
2034 Ok(())
2035 }
2036
2037 #[pyo3(name = "subscribe_option_chain")]
2038 #[pyo3(signature = (series_id, strike_range, snapshot_interval_ms=None, client_id=None, params=None))]
2039 fn py_subscribe_option_chain(
2040 &mut self,
2041 py: Python<'_>,
2042 series_id: OptionSeriesId,
2043 strike_range: PyStrikeRange,
2044 snapshot_interval_ms: Option<u64>,
2045 client_id: Option<ClientId>,
2046 params: Option<Py<PyDict>>,
2047 ) -> PyResult<()> {
2048 self.ensure_registered()?;
2049 let params = dict_to_params(py, params)?;
2050 DataActor::subscribe_option_chain(
2051 self.inner_mut(),
2052 series_id,
2053 strike_range.inner,
2054 snapshot_interval_ms,
2055 client_id,
2056 params,
2057 );
2058 Ok(())
2059 }
2060
2061 #[pyo3(name = "unsubscribe_data")]
2062 #[pyo3(signature = (data_type, client_id=None, params=None))]
2063 fn py_unsubscribe_data(
2064 &mut self,
2065 py: Python<'_>,
2066 data_type: DataType,
2067 client_id: Option<ClientId>,
2068 params: Option<Py<PyDict>>,
2069 ) -> PyResult<()> {
2070 self.ensure_registered()?;
2071 let params = dict_to_params(py, params)?;
2072 DataActor::unsubscribe_data(self.inner_mut(), data_type, client_id, params);
2073 Ok(())
2074 }
2075
2076 #[pyo3(name = "unsubscribe_signal")]
2077 #[pyo3(signature = (name=""))]
2078 fn py_unsubscribe_signal(slf: &Bound<'_, Self>, name: &str) -> PyResult<()> {
2079 let actor = borrow_actor_mut(slf, "unsubscribe_signal")?;
2080 actor.ensure_registered()?;
2081 DataActor::unsubscribe_signal(actor.inner_mut(), name);
2082 Ok(())
2083 }
2084
2085 #[pyo3(name = "unsubscribe_queue_state")]
2086 #[pyo3(signature = (channel=None))]
2087 fn py_unsubscribe_queue_state(&mut self, channel: Option<SystemChannel>) -> PyResult<()> {
2088 self.ensure_registered()?;
2089 DataActor::unsubscribe_queue_state(self.inner_mut(), channel);
2090 Ok(())
2091 }
2092
2093 #[pyo3(name = "unsubscribe_socket_state")]
2094 #[pyo3(signature = (client_id=None, endpoint=None))]
2095 fn py_unsubscribe_socket_state(
2096 &mut self,
2097 client_id: Option<ClientId>,
2098 endpoint: Option<&str>,
2099 ) -> PyResult<()> {
2100 self.ensure_registered()?;
2101 DataActor::unsubscribe_socket_state(self.inner_mut(), client_id, endpoint);
2102 Ok(())
2103 }
2104
2105 #[pyo3(name = "unsubscribe_instruments")]
2106 #[pyo3(signature = (venue, client_id=None, params=None))]
2107 fn py_unsubscribe_instruments(
2108 &mut self,
2109 py: Python<'_>,
2110 venue: Venue,
2111 client_id: Option<ClientId>,
2112 params: Option<Py<PyDict>>,
2113 ) -> PyResult<()> {
2114 self.ensure_registered()?;
2115 let params = dict_to_params(py, params)?;
2116 DataActor::unsubscribe_instruments(self.inner_mut(), venue, client_id, params);
2117 Ok(())
2118 }
2119
2120 #[pyo3(name = "unsubscribe_instrument")]
2121 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2122 fn py_unsubscribe_instrument(
2123 &mut self,
2124 py: Python<'_>,
2125 instrument_id: InstrumentId,
2126 client_id: Option<ClientId>,
2127 params: Option<Py<PyDict>>,
2128 ) -> PyResult<()> {
2129 self.ensure_registered()?;
2130 let params = dict_to_params(py, params)?;
2131 DataActor::unsubscribe_instrument(self.inner_mut(), instrument_id, client_id, params);
2132 Ok(())
2133 }
2134
2135 #[pyo3(name = "unsubscribe_book_deltas")]
2136 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2137 fn py_unsubscribe_book_deltas(
2138 &mut self,
2139 py: Python<'_>,
2140 instrument_id: InstrumentId,
2141 client_id: Option<ClientId>,
2142 params: Option<Py<PyDict>>,
2143 ) -> PyResult<()> {
2144 self.ensure_registered()?;
2145 let params = dict_to_params(py, params)?;
2146 DataActor::unsubscribe_book_deltas(self.inner_mut(), instrument_id, client_id, params);
2147 Ok(())
2148 }
2149
2150 #[pyo3(name = "unsubscribe_book_depth")]
2151 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2152 fn py_unsubscribe_book_depth(
2153 &mut self,
2154 py: Python<'_>,
2155 instrument_id: InstrumentId,
2156 client_id: Option<ClientId>,
2157 params: Option<Py<PyDict>>,
2158 ) -> PyResult<()> {
2159 self.ensure_registered()?;
2160 let params = dict_to_params(py, params)?;
2161 DataActor::unsubscribe_book_depth(self.inner_mut(), instrument_id, client_id, params);
2162 Ok(())
2163 }
2164
2165 #[pyo3(name = "unsubscribe_book_at_interval")]
2166 #[pyo3(signature = (instrument_id, interval_ms, client_id=None, params=None))]
2167 fn py_unsubscribe_book_at_interval(
2168 &mut self,
2169 py: Python<'_>,
2170 instrument_id: InstrumentId,
2171 interval_ms: usize,
2172 client_id: Option<ClientId>,
2173 params: Option<Py<PyDict>>,
2174 ) -> PyResult<()> {
2175 let interval_ms = NonZeroUsize::new(interval_ms)
2176 .ok_or_else(|| to_pyvalue_err("interval_ms must be > 0"))?;
2177
2178 self.ensure_registered()?;
2179 let params = dict_to_params(py, params)?;
2180 DataActor::unsubscribe_book_at_interval(
2181 self.inner_mut(),
2182 instrument_id,
2183 interval_ms,
2184 client_id,
2185 params,
2186 );
2187 Ok(())
2188 }
2189
2190 #[pyo3(name = "unsubscribe_quotes")]
2191 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2192 fn py_unsubscribe_quotes(
2193 &mut self,
2194 py: Python<'_>,
2195 instrument_id: InstrumentId,
2196 client_id: Option<ClientId>,
2197 params: Option<Py<PyDict>>,
2198 ) -> PyResult<()> {
2199 self.ensure_registered()?;
2200 let params = dict_to_params(py, params)?;
2201 DataActor::unsubscribe_quotes(self.inner_mut(), instrument_id, client_id, params);
2202 Ok(())
2203 }
2204
2205 #[pyo3(name = "unsubscribe_trades")]
2206 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2207 fn py_unsubscribe_trades(
2208 &mut self,
2209 py: Python<'_>,
2210 instrument_id: InstrumentId,
2211 client_id: Option<ClientId>,
2212 params: Option<Py<PyDict>>,
2213 ) -> PyResult<()> {
2214 self.ensure_registered()?;
2215 let params = dict_to_params(py, params)?;
2216 DataActor::unsubscribe_trades(self.inner_mut(), instrument_id, client_id, params);
2217 Ok(())
2218 }
2219
2220 #[pyo3(name = "unsubscribe_bars")]
2221 #[pyo3(signature = (bar_type, client_id=None, params=None))]
2222 fn py_unsubscribe_bars(
2223 &mut self,
2224 py: Python<'_>,
2225 bar_type: BarType,
2226 client_id: Option<ClientId>,
2227 params: Option<Py<PyDict>>,
2228 ) -> PyResult<()> {
2229 self.ensure_registered()?;
2230 let params = dict_to_params(py, params)?;
2231 DataActor::unsubscribe_bars(self.inner_mut(), bar_type, client_id, params);
2232 Ok(())
2233 }
2234
2235 #[pyo3(name = "unsubscribe_mark_prices")]
2236 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2237 fn py_unsubscribe_mark_prices(
2238 &mut self,
2239 py: Python<'_>,
2240 instrument_id: InstrumentId,
2241 client_id: Option<ClientId>,
2242 params: Option<Py<PyDict>>,
2243 ) -> PyResult<()> {
2244 self.ensure_registered()?;
2245 let params = dict_to_params(py, params)?;
2246 DataActor::unsubscribe_mark_prices(self.inner_mut(), instrument_id, client_id, params);
2247 Ok(())
2248 }
2249
2250 #[pyo3(name = "unsubscribe_index_prices")]
2251 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2252 fn py_unsubscribe_index_prices(
2253 &mut self,
2254 py: Python<'_>,
2255 instrument_id: InstrumentId,
2256 client_id: Option<ClientId>,
2257 params: Option<Py<PyDict>>,
2258 ) -> PyResult<()> {
2259 self.ensure_registered()?;
2260 let params = dict_to_params(py, params)?;
2261 DataActor::unsubscribe_index_prices(self.inner_mut(), instrument_id, client_id, params);
2262 Ok(())
2263 }
2264
2265 #[pyo3(name = "unsubscribe_funding_rates")]
2266 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2267 fn py_unsubscribe_funding_rates(
2268 &mut self,
2269 py: Python<'_>,
2270 instrument_id: InstrumentId,
2271 client_id: Option<ClientId>,
2272 params: Option<Py<PyDict>>,
2273 ) -> PyResult<()> {
2274 self.ensure_registered()?;
2275 let params = dict_to_params(py, params)?;
2276 DataActor::unsubscribe_funding_rates(self.inner_mut(), instrument_id, client_id, params);
2277 Ok(())
2278 }
2279
2280 #[pyo3(name = "unsubscribe_option_greeks")]
2281 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2282 fn py_unsubscribe_option_greeks(
2283 &mut self,
2284 py: Python<'_>,
2285 instrument_id: InstrumentId,
2286 client_id: Option<ClientId>,
2287 params: Option<Py<PyDict>>,
2288 ) -> PyResult<()> {
2289 self.ensure_registered()?;
2290 let params = dict_to_params(py, params)?;
2291 DataActor::unsubscribe_option_greeks(self.inner_mut(), instrument_id, client_id, params);
2292 Ok(())
2293 }
2294
2295 #[pyo3(name = "unsubscribe_instrument_status")]
2296 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2297 fn py_unsubscribe_instrument_status(
2298 &mut self,
2299 py: Python<'_>,
2300 instrument_id: InstrumentId,
2301 client_id: Option<ClientId>,
2302 params: Option<Py<PyDict>>,
2303 ) -> PyResult<()> {
2304 self.ensure_registered()?;
2305 let params = dict_to_params(py, params)?;
2306 DataActor::unsubscribe_instrument_status(
2307 self.inner_mut(),
2308 instrument_id,
2309 client_id,
2310 params,
2311 );
2312 Ok(())
2313 }
2314
2315 #[pyo3(name = "unsubscribe_instrument_close")]
2316 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2317 fn py_unsubscribe_instrument_close(
2318 &mut self,
2319 py: Python<'_>,
2320 instrument_id: InstrumentId,
2321 client_id: Option<ClientId>,
2322 params: Option<Py<PyDict>>,
2323 ) -> PyResult<()> {
2324 self.ensure_registered()?;
2325 let params = dict_to_params(py, params)?;
2326 DataActor::unsubscribe_instrument_close(self.inner_mut(), instrument_id, client_id, params);
2327 Ok(())
2328 }
2329
2330 #[pyo3(name = "unsubscribe_option_chain")]
2331 #[pyo3(signature = (series_id, client_id=None))]
2332 fn py_unsubscribe_option_chain(
2333 &mut self,
2334 series_id: OptionSeriesId,
2335 client_id: Option<ClientId>,
2336 ) -> PyResult<()> {
2337 self.ensure_registered()?;
2338 DataActor::unsubscribe_option_chain(self.inner_mut(), series_id, client_id);
2339 Ok(())
2340 }
2341
2342 #[pyo3(name = "request_data")]
2343 #[pyo3(signature = (data_type, client_id, start=None, end=None, limit=None, params=None))]
2344 #[expect(clippy::too_many_arguments)]
2345 fn py_request_data(
2346 &mut self,
2347 py: Python<'_>,
2348 data_type: DataType,
2349 client_id: ClientId,
2350 start: Option<Timestamp>,
2351 end: Option<Timestamp>,
2352 limit: Option<usize>,
2353 params: Option<Py<PyDict>>,
2354 ) -> PyResult<String> {
2355 self.ensure_registered_for_data()?;
2356 let params = dict_to_params(py, params)?;
2357 let limit = limit.and_then(NonZeroUsize::new);
2358 let request_id = DataActor::request_data(
2359 self.inner_mut(),
2360 data_type,
2361 client_id,
2362 start,
2363 end,
2364 limit,
2365 params,
2366 )
2367 .map_err(to_pyvalue_err)?;
2368 Ok(request_id.to_string())
2369 }
2370
2371 #[pyo3(name = "request_instrument")]
2372 #[pyo3(signature = (instrument_id, start=None, end=None, client_id=None, params=None))]
2373 fn py_request_instrument(
2374 &mut self,
2375 py: Python<'_>,
2376 instrument_id: InstrumentId,
2377 start: Option<Timestamp>,
2378 end: Option<Timestamp>,
2379 client_id: Option<ClientId>,
2380 params: Option<Py<PyDict>>,
2381 ) -> PyResult<String> {
2382 self.ensure_registered_for_data()?;
2383 let params = dict_to_params(py, params)?;
2384 let request_id = DataActor::request_instrument(
2385 self.inner_mut(),
2386 instrument_id,
2387 start,
2388 end,
2389 client_id,
2390 params,
2391 )
2392 .map_err(to_pyvalue_err)?;
2393 Ok(request_id.to_string())
2394 }
2395
2396 #[pyo3(name = "request_instruments")]
2397 #[pyo3(signature = (venue=None, start=None, end=None, client_id=None, params=None))]
2398 fn py_request_instruments(
2399 &mut self,
2400 py: Python<'_>,
2401 venue: Option<Venue>,
2402 start: Option<Timestamp>,
2403 end: Option<Timestamp>,
2404 client_id: Option<ClientId>,
2405 params: Option<Py<PyDict>>,
2406 ) -> PyResult<String> {
2407 self.ensure_registered_for_data()?;
2408 let params = dict_to_params(py, params)?;
2409 let request_id =
2410 DataActor::request_instruments(self.inner_mut(), venue, start, end, client_id, params)
2411 .map_err(to_pyvalue_err)?;
2412 Ok(request_id.to_string())
2413 }
2414
2415 #[pyo3(name = "request_book_snapshot")]
2416 #[pyo3(signature = (instrument_id, depth=None, client_id=None, params=None))]
2417 fn py_request_book_snapshot(
2418 &mut self,
2419 py: Python<'_>,
2420 instrument_id: InstrumentId,
2421 depth: Option<usize>,
2422 client_id: Option<ClientId>,
2423 params: Option<Py<PyDict>>,
2424 ) -> PyResult<String> {
2425 self.ensure_registered_for_data()?;
2426 let params = dict_to_params(py, params)?;
2427 let depth = depth.and_then(NonZeroUsize::new);
2428
2429 let request_id = DataActor::request_book_snapshot(
2430 self.inner_mut(),
2431 instrument_id,
2432 depth,
2433 client_id,
2434 params,
2435 )
2436 .map_err(to_pyvalue_err)?;
2437 Ok(request_id.to_string())
2438 }
2439
2440 #[pyo3(name = "request_book_deltas")]
2441 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None))]
2442 #[expect(clippy::too_many_arguments)]
2443 fn py_request_book_deltas(
2444 &mut self,
2445 py: Python<'_>,
2446 instrument_id: InstrumentId,
2447 start: Option<Timestamp>,
2448 end: Option<Timestamp>,
2449 limit: Option<usize>,
2450 client_id: Option<ClientId>,
2451 params: Option<Py<PyDict>>,
2452 ) -> PyResult<String> {
2453 self.ensure_registered_for_data()?;
2454 let params = dict_to_params(py, params)?;
2455 let limit = limit.and_then(NonZeroUsize::new);
2456 let request_id = DataActor::request_book_deltas(
2457 self.inner_mut(),
2458 instrument_id,
2459 start,
2460 end,
2461 limit,
2462 client_id,
2463 params,
2464 )
2465 .map_err(to_pyvalue_err)?;
2466 Ok(request_id.to_string())
2467 }
2468
2469 #[pyo3(name = "request_book_depth")]
2470 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, depth=None, client_id=None, params=None))]
2471 #[expect(clippy::too_many_arguments)]
2472 fn py_request_book_depth(
2473 &mut self,
2474 py: Python<'_>,
2475 instrument_id: InstrumentId,
2476 start: Option<Timestamp>,
2477 end: Option<Timestamp>,
2478 limit: Option<usize>,
2479 depth: Option<usize>,
2480 client_id: Option<ClientId>,
2481 params: Option<Py<PyDict>>,
2482 ) -> PyResult<String> {
2483 self.ensure_registered_for_data()?;
2484 let params = dict_to_params(py, params)?;
2485 let limit = limit.and_then(NonZeroUsize::new);
2486 let depth = depth.and_then(NonZeroUsize::new);
2487 let request_id = DataActor::request_book_depth(
2488 self.inner_mut(),
2489 instrument_id,
2490 start,
2491 end,
2492 limit,
2493 depth,
2494 client_id,
2495 params,
2496 )
2497 .map_err(to_pyvalue_err)?;
2498 Ok(request_id.to_string())
2499 }
2500
2501 #[pyo3(name = "request_quotes")]
2502 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None))]
2503 #[expect(clippy::too_many_arguments)]
2504 fn py_request_quotes(
2505 &mut self,
2506 py: Python<'_>,
2507 instrument_id: InstrumentId,
2508 start: Option<Timestamp>,
2509 end: Option<Timestamp>,
2510 limit: Option<usize>,
2511 client_id: Option<ClientId>,
2512 params: Option<Py<PyDict>>,
2513 ) -> PyResult<String> {
2514 self.ensure_registered_for_data()?;
2515 let params = dict_to_params(py, params)?;
2516 let limit = limit.and_then(NonZeroUsize::new);
2517 let request_id = DataActor::request_quotes(
2518 self.inner_mut(),
2519 instrument_id,
2520 start,
2521 end,
2522 limit,
2523 client_id,
2524 params,
2525 )
2526 .map_err(to_pyvalue_err)?;
2527 Ok(request_id.to_string())
2528 }
2529
2530 #[pyo3(name = "request_trades")]
2531 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None))]
2532 #[expect(clippy::too_many_arguments)]
2533 fn py_request_trades(
2534 &mut self,
2535 py: Python<'_>,
2536 instrument_id: InstrumentId,
2537 start: Option<Timestamp>,
2538 end: Option<Timestamp>,
2539 limit: Option<usize>,
2540 client_id: Option<ClientId>,
2541 params: Option<Py<PyDict>>,
2542 ) -> PyResult<String> {
2543 self.ensure_registered_for_data()?;
2544 let params = dict_to_params(py, params)?;
2545 let limit = limit.and_then(NonZeroUsize::new);
2546 let request_id = DataActor::request_trades(
2547 self.inner_mut(),
2548 instrument_id,
2549 start,
2550 end,
2551 limit,
2552 client_id,
2553 params,
2554 )
2555 .map_err(to_pyvalue_err)?;
2556 Ok(request_id.to_string())
2557 }
2558
2559 #[pyo3(name = "request_funding_rates")]
2560 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None))]
2561 #[expect(clippy::too_many_arguments)]
2562 fn py_request_funding_rates(
2563 &mut self,
2564 py: Python<'_>,
2565 instrument_id: InstrumentId,
2566 start: Option<Timestamp>,
2567 end: Option<Timestamp>,
2568 limit: Option<usize>,
2569 client_id: Option<ClientId>,
2570 params: Option<Py<PyDict>>,
2571 ) -> PyResult<String> {
2572 self.ensure_registered_for_data()?;
2573 let params = dict_to_params(py, params)?;
2574 let limit = limit.and_then(NonZeroUsize::new);
2575 let request_id = DataActor::request_funding_rates(
2576 self.inner_mut(),
2577 instrument_id,
2578 start,
2579 end,
2580 limit,
2581 client_id,
2582 params,
2583 )
2584 .map_err(to_pyvalue_err)?;
2585 Ok(request_id.to_string())
2586 }
2587
2588 #[pyo3(name = "request_bars")]
2589 #[pyo3(signature = (bar_type, start=None, end=None, limit=None, client_id=None, params=None))]
2590 #[expect(clippy::too_many_arguments)]
2591 fn py_request_bars(
2592 &mut self,
2593 py: Python<'_>,
2594 bar_type: BarType,
2595 start: Option<Timestamp>,
2596 end: Option<Timestamp>,
2597 limit: Option<usize>,
2598 client_id: Option<ClientId>,
2599 params: Option<Py<PyDict>>,
2600 ) -> PyResult<String> {
2601 self.ensure_registered_for_data()?;
2602 let params = dict_to_params(py, params)?;
2603 let limit = limit.and_then(NonZeroUsize::new);
2604 let request_id = DataActor::request_bars(
2605 self.inner_mut(),
2606 bar_type,
2607 start,
2608 end,
2609 limit,
2610 client_id,
2611 params,
2612 )
2613 .map_err(to_pyvalue_err)?;
2614 Ok(request_id.to_string())
2615 }
2616
2617 #[pyo3(name = "reconnect_socket")]
2619 fn py_reconnect_socket(&self, client_id: ClientId, endpoint: &str) -> PyResult<()> {
2620 DataActor::reconnect_socket(self.inner(), client_id, endpoint).map_err(to_pyruntime_err)
2621 }
2622
2623 #[allow(unused_variables, clippy::needless_pass_by_value)]
2624 #[pyo3(name = "on_historical_data")]
2625 fn py_on_historical_data(_slf: &Bound<'_, Self>, data: Py<PyAny>) {
2626 }
2628
2629 #[allow(unused_variables, clippy::needless_pass_by_value)]
2630 #[pyo3(name = "on_historical_book_deltas")]
2631 fn py_on_historical_book_deltas(_slf: &Bound<'_, Self>, deltas: Vec<OrderBookDelta>) {}
2632
2633 #[allow(unused_variables, clippy::needless_pass_by_value)]
2634 #[pyo3(name = "on_historical_book_depth")]
2635 fn py_on_historical_book_depth(_slf: &Bound<'_, Self>, depths: Vec<OrderBookDepth>) {}
2636
2637 #[allow(unused_variables, clippy::needless_pass_by_value)]
2638 #[pyo3(name = "on_historical_quotes")]
2639 fn py_on_historical_quotes(_slf: &Bound<'_, Self>, quotes: Vec<QuoteTick>) {
2640 }
2642
2643 #[allow(unused_variables, clippy::needless_pass_by_value)]
2644 #[pyo3(name = "on_historical_trades")]
2645 fn py_on_historical_trades(_slf: &Bound<'_, Self>, trades: Vec<TradeTick>) {
2646 }
2648
2649 #[allow(unused_variables, clippy::needless_pass_by_value)]
2650 #[pyo3(name = "on_historical_funding_rates")]
2651 fn py_on_historical_funding_rates(
2652 _slf: &Bound<'_, Self>,
2653 funding_rates: Vec<FundingRateUpdate>,
2654 ) {
2655 }
2657
2658 #[allow(unused_variables, clippy::needless_pass_by_value)]
2659 #[pyo3(name = "on_historical_bars")]
2660 fn py_on_historical_bars(_slf: &Bound<'_, Self>, bars: Vec<Bar>) {
2661 }
2663
2664 #[allow(unused_variables, clippy::needless_pass_by_value)]
2665 #[pyo3(name = "on_historical_mark_prices")]
2666 fn py_on_historical_mark_prices(_slf: &Bound<'_, Self>, mark_prices: Vec<MarkPriceUpdate>) {
2667 }
2669
2670 #[allow(unused_variables, clippy::needless_pass_by_value)]
2671 #[pyo3(name = "on_historical_index_prices")]
2672 fn py_on_historical_index_prices(_slf: &Bound<'_, Self>, index_prices: Vec<IndexPriceUpdate>) {
2673 }
2675}
2676
2677#[pyo3_stub_gen::derive::gen_stub_pymethods]
2678#[pyo3::pymethods]
2679impl PyDataActor {
2680 #[pyo3(name = "publish_message", signature = (topic, message))]
2681 fn py_publish_message(
2682 slf: &Bound<'_, Self>,
2683 topic: &str,
2684 #[gen_stub(override_type(type_repr = "object"))] message: Py<PyAny>,
2685 ) -> PyResult<()> {
2686 let messages = get_python_message_bus(slf.as_any())?;
2687 messages.publish_message(topic, message)
2688 }
2689
2690 #[pyo3(name = "subscribe_topic")]
2691 #[pyo3(signature = (topic, handler, priority=0))]
2692 fn py_subscribe_topic(
2693 slf: &Bound<'_, Self>,
2694 topic: &str,
2695 #[gen_stub(override_type(type_repr = "collections.abc.Callable[[object], None]", imports = ("collections.abc",)))]
2696 handler: Py<PyAny>,
2697 priority: u32,
2698 ) -> PyResult<()> {
2699 let messages = get_python_message_bus(slf.as_any())?;
2700 messages.subscribe_topic(slf.py(), topic, handler, priority)
2701 }
2702
2703 #[pyo3(name = "unsubscribe_topic", signature = (topic, handler))]
2704 fn py_unsubscribe_topic(
2705 slf: &Bound<'_, Self>,
2706 topic: &str,
2707 #[gen_stub(override_type(type_repr = "collections.abc.Callable[[object], None]", imports = ("collections.abc",)))]
2708 handler: &Bound<'_, PyAny>,
2709 ) -> PyResult<()> {
2710 let messages = get_python_message_bus(slf.as_any())?;
2711 messages.unsubscribe_topic(topic, handler)
2712 }
2713}
2714
2715#[cfg(feature = "defi")]
2716#[pymethods]
2717#[pyo3_stub_gen::derive::gen_stub_pymethods]
2718impl PyDataActor {
2719 #[pyo3(name = "on_block")]
2720 #[allow(unused_variables, clippy::needless_pass_by_value)]
2721 fn py_on_block(_slf: &Bound<'_, Self>, block: Block) {}
2722
2723 #[pyo3(name = "on_pool")]
2724 #[allow(unused_variables, clippy::needless_pass_by_value)]
2725 fn py_on_pool(_slf: &Bound<'_, Self>, pool: Pool) {}
2726
2727 #[pyo3(name = "on_pool_swap")]
2728 #[allow(unused_variables, clippy::needless_pass_by_value)]
2729 fn py_on_pool_swap(_slf: &Bound<'_, Self>, swap: PoolSwap) {}
2730
2731 #[pyo3(name = "on_pool_liquidity_update")]
2732 #[allow(unused_variables, clippy::needless_pass_by_value)]
2733 fn py_on_pool_liquidity_update(_slf: &Bound<'_, Self>, update: PoolLiquidityUpdate) {}
2734
2735 #[pyo3(name = "on_pool_fee_collect")]
2736 #[allow(unused_variables, clippy::needless_pass_by_value)]
2737 fn py_on_pool_fee_collect(_slf: &Bound<'_, Self>, update: PoolFeeCollect) {}
2738
2739 #[pyo3(name = "on_pool_flash")]
2740 #[allow(unused_variables, clippy::needless_pass_by_value)]
2741 fn py_on_pool_flash(_slf: &Bound<'_, Self>, flash: PoolFlash) {}
2742
2743 #[pyo3(name = "subscribe_blocks")]
2744 #[pyo3(signature = (chain, client_id=None, params=None))]
2745 fn py_subscribe_blocks(
2746 &mut self,
2747 py: Python<'_>,
2748 chain: Blockchain,
2749 client_id: Option<ClientId>,
2750 params: Option<Py<PyDict>>,
2751 ) -> PyResult<()> {
2752 self.ensure_registered()?;
2753 let params = dict_to_params(py, params)?;
2754 DataActor::subscribe_blocks(self.inner_mut(), chain, client_id, params);
2755 Ok(())
2756 }
2757
2758 #[pyo3(name = "subscribe_pool")]
2759 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2760 fn py_subscribe_pool(
2761 &mut self,
2762 py: Python<'_>,
2763 instrument_id: InstrumentId,
2764 client_id: Option<ClientId>,
2765 params: Option<Py<PyDict>>,
2766 ) -> PyResult<()> {
2767 self.ensure_registered()?;
2768 let params = dict_to_params(py, params)?;
2769 DataActor::subscribe_pool(self.inner_mut(), instrument_id, client_id, params);
2770 Ok(())
2771 }
2772
2773 #[pyo3(name = "subscribe_pool_swaps")]
2774 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2775 fn py_subscribe_pool_swaps(
2776 &mut self,
2777 py: Python<'_>,
2778 instrument_id: InstrumentId,
2779 client_id: Option<ClientId>,
2780 params: Option<Py<PyDict>>,
2781 ) -> PyResult<()> {
2782 self.ensure_registered()?;
2783 let params = dict_to_params(py, params)?;
2784 DataActor::subscribe_pool_swaps(self.inner_mut(), instrument_id, client_id, params);
2785 Ok(())
2786 }
2787
2788 #[pyo3(name = "subscribe_pool_liquidity_updates")]
2789 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2790 fn py_subscribe_pool_liquidity_updates(
2791 &mut self,
2792 py: Python<'_>,
2793 instrument_id: InstrumentId,
2794 client_id: Option<ClientId>,
2795 params: Option<Py<PyDict>>,
2796 ) -> PyResult<()> {
2797 self.ensure_registered()?;
2798 let params = dict_to_params(py, params)?;
2799 DataActor::subscribe_pool_liquidity_updates(
2800 self.inner_mut(),
2801 instrument_id,
2802 client_id,
2803 params,
2804 );
2805 Ok(())
2806 }
2807
2808 #[pyo3(name = "subscribe_pool_fee_collects")]
2809 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2810 fn py_subscribe_pool_fee_collects(
2811 &mut self,
2812 py: Python<'_>,
2813 instrument_id: InstrumentId,
2814 client_id: Option<ClientId>,
2815 params: Option<Py<PyDict>>,
2816 ) -> PyResult<()> {
2817 self.ensure_registered()?;
2818 let params = dict_to_params(py, params)?;
2819 DataActor::subscribe_pool_fee_collects(self.inner_mut(), instrument_id, client_id, params);
2820 Ok(())
2821 }
2822
2823 #[pyo3(name = "subscribe_pool_flash_events")]
2824 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2825 fn py_subscribe_pool_flash_events(
2826 &mut self,
2827 py: Python<'_>,
2828 instrument_id: InstrumentId,
2829 client_id: Option<ClientId>,
2830 params: Option<Py<PyDict>>,
2831 ) -> PyResult<()> {
2832 self.ensure_registered()?;
2833 let params = dict_to_params(py, params)?;
2834 DataActor::subscribe_pool_flash_events(self.inner_mut(), instrument_id, client_id, params);
2835 Ok(())
2836 }
2837
2838 #[pyo3(name = "unsubscribe_blocks")]
2839 #[pyo3(signature = (chain, client_id=None, params=None))]
2840 fn py_unsubscribe_blocks(
2841 &mut self,
2842 py: Python<'_>,
2843 chain: Blockchain,
2844 client_id: Option<ClientId>,
2845 params: Option<Py<PyDict>>,
2846 ) -> PyResult<()> {
2847 self.ensure_registered()?;
2848 let params = dict_to_params(py, params)?;
2849 DataActor::unsubscribe_blocks(self.inner_mut(), chain, client_id, params);
2850 Ok(())
2851 }
2852
2853 #[pyo3(name = "unsubscribe_pool")]
2854 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2855 fn py_unsubscribe_pool(
2856 &mut self,
2857 py: Python<'_>,
2858 instrument_id: InstrumentId,
2859 client_id: Option<ClientId>,
2860 params: Option<Py<PyDict>>,
2861 ) -> PyResult<()> {
2862 self.ensure_registered()?;
2863 let params = dict_to_params(py, params)?;
2864 DataActor::unsubscribe_pool(self.inner_mut(), instrument_id, client_id, params);
2865 Ok(())
2866 }
2867
2868 #[pyo3(name = "unsubscribe_pool_swaps")]
2869 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2870 fn py_unsubscribe_pool_swaps(
2871 &mut self,
2872 py: Python<'_>,
2873 instrument_id: InstrumentId,
2874 client_id: Option<ClientId>,
2875 params: Option<Py<PyDict>>,
2876 ) -> PyResult<()> {
2877 self.ensure_registered()?;
2878 let params = dict_to_params(py, params)?;
2879 DataActor::unsubscribe_pool_swaps(self.inner_mut(), instrument_id, client_id, params);
2880 Ok(())
2881 }
2882
2883 #[pyo3(name = "unsubscribe_pool_liquidity_updates")]
2884 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2885 fn py_unsubscribe_pool_liquidity_updates(
2886 &mut self,
2887 py: Python<'_>,
2888 instrument_id: InstrumentId,
2889 client_id: Option<ClientId>,
2890 params: Option<Py<PyDict>>,
2891 ) -> PyResult<()> {
2892 self.ensure_registered()?;
2893 let params = dict_to_params(py, params)?;
2894 DataActor::unsubscribe_pool_liquidity_updates(
2895 self.inner_mut(),
2896 instrument_id,
2897 client_id,
2898 params,
2899 );
2900 Ok(())
2901 }
2902
2903 #[pyo3(name = "unsubscribe_pool_fee_collects")]
2904 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2905 fn py_unsubscribe_pool_fee_collects(
2906 &mut self,
2907 py: Python<'_>,
2908 instrument_id: InstrumentId,
2909 client_id: Option<ClientId>,
2910 params: Option<Py<PyDict>>,
2911 ) -> PyResult<()> {
2912 self.ensure_registered()?;
2913 let params = dict_to_params(py, params)?;
2914 DataActor::unsubscribe_pool_fee_collects(
2915 self.inner_mut(),
2916 instrument_id,
2917 client_id,
2918 params,
2919 );
2920 Ok(())
2921 }
2922
2923 #[pyo3(name = "unsubscribe_pool_flash_events")]
2924 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2925 fn py_unsubscribe_pool_flash_events(
2926 &mut self,
2927 py: Python<'_>,
2928 instrument_id: InstrumentId,
2929 client_id: Option<ClientId>,
2930 params: Option<Py<PyDict>>,
2931 ) -> PyResult<()> {
2932 self.ensure_registered()?;
2933 let params = dict_to_params(py, params)?;
2934 DataActor::unsubscribe_pool_flash_events(
2935 self.inner_mut(),
2936 instrument_id,
2937 client_id,
2938 params,
2939 );
2940 Ok(())
2941 }
2942}
2943
2944impl PyDataActor {
2945 fn ensure_registered_for_data(&self) -> PyResult<()> {
2946 if self.inner().core.is_registered() {
2947 Ok(())
2948 } else {
2949 Err(to_pyruntime_err(
2950 "DataActor must be registered before publishing, managing synthetics, or requesting data",
2951 ))
2952 }
2953 }
2954
2955 fn ensure_registered(&self) -> PyResult<()> {
2956 if self.inner().core.is_registered() {
2957 Ok(())
2958 } else {
2959 Err(to_pyruntime_err(
2960 "DataActor must be registered before managing subscriptions",
2961 ))
2962 }
2963 }
2964}
2965
2966pub fn prepare_python_actor(
2980 actor_obj: &Bound<'_, PyAny>,
2981 config: Option<&Bound<'_, PyAny>>,
2982) -> anyhow::Result<ActorId> {
2983 let mut actor = actor_obj
2984 .extract::<PyRefMut<PyDataActor>>()
2985 .map_err(Into::<PyErr>::into)
2986 .map_err(|e| anyhow::anyhow!("Failed to extract PyDataActor: {e}"))?;
2987
2988 if let Some(config) = config {
2989 if let Some(actor_id) = config
2990 .getattr("actor_id")
2991 .ok()
2992 .filter(|actor_id| !actor_id.is_none())
2993 {
2994 let actor_id = if let Ok(actor_id) = actor_id.extract::<ActorId>() {
2995 actor_id
2996 } else if let Ok(actor_id) = actor_id.extract::<String>() {
2997 ActorId::new_checked(&actor_id)?
2998 } else {
2999 anyhow::bail!("Invalid `actor_id` type");
3000 };
3001 actor.set_actor_id(actor_id);
3002 }
3003
3004 if let Some(log_events) = extract_bool_config_attr(config, "log_events") {
3005 actor.set_log_events(log_events);
3006 }
3007
3008 if let Some(log_commands) = extract_bool_config_attr(config, "log_commands") {
3009 actor.set_log_commands(log_commands);
3010 }
3011 }
3012
3013 actor.set_python_instance(actor_obj)?;
3014 apply_class_derived_actor_id(&mut actor, actor_obj)?;
3015 Ok(actor.actor_id())
3016}
3017
3018fn apply_class_derived_actor_id(
3019 actor: &mut PyRefMut<'_, PyDataActor>,
3020 actor_obj: &Bound<'_, PyAny>,
3021) -> PyResult<()> {
3022 if actor.inner().core.config.actor_id.is_some() {
3023 return Ok(());
3024 }
3025
3026 let py_type = actor_obj.get_type();
3027 let type_name = py_type.name()?;
3028 let actor_id = ActorId::new_checked(type_name.to_str()?).map_err(to_pyvalue_err)?;
3029 actor.set_actor_id(actor_id);
3030
3031 Ok(())
3032}
3033
3034fn extract_bool_config_attr(config: &Bound<'_, PyAny>, attr: &str) -> Option<bool> {
3035 config
3036 .getattr(attr)
3037 .ok()
3038 .and_then(|value| value.extract::<bool>().ok())
3039}
3040
3041fn borrow_actor_mut<'py>(
3042 actor: &Bound<'py, PyDataActor>,
3043 operation: &'static str,
3044) -> PyResult<PyRefMut<'py, PyDataActor>> {
3045 actor.try_borrow_mut().map_err(|_| {
3046 to_pyruntime_err(ComponentAccessError::WriteConflict {
3047 resource: "Python actor",
3048 operation,
3049 })
3050 })
3051}
3052
3053fn has_configured_actor_id(slf: &Bound<'_, PyDataActor>) -> bool {
3059 let py = slf.py();
3060 let config = slf
3061 .borrow()
3062 .inner()
3063 .config
3064 .as_ref()
3065 .map(|config| config.clone_ref(py));
3066
3067 config.is_some_and(|config| {
3068 config
3069 .bind(py)
3070 .getattr("actor_id")
3071 .is_ok_and(|actor_id| !actor_id.is_none())
3072 })
3073}
3074
3075#[cfg(test)]
3076mod tests {
3077 use std::{cell::RefCell, collections::HashMap, rc::Rc, str::FromStr, sync::Arc};
3078
3079 #[cfg(feature = "defi")]
3080 use alloy_primitives::{I256, U160, U256};
3081 use indexmap::IndexMap;
3082 use nautilus_core::{UUID4, UnixNanos, python::IntoPyObjectNautilusExt};
3083 #[cfg(feature = "defi")]
3084 use nautilus_model::defi::{
3085 AmmType, Block, Blockchain, Chain, Dex, DexType, Pool, PoolFeeCollect, PoolFlash,
3086 PoolIdentifier, PoolLiquidityUpdate, PoolLiquidityUpdateType, PoolSwap, Token,
3087 };
3088 use nautilus_model::{
3089 data::{
3090 Bar, BarType, CustomData, DataType, FundingRateUpdate, IndexPriceUpdate,
3091 InstrumentStatus, MarkPriceUpdate, OrderBookDelta, OrderBookDeltas, OrderBookDepth,
3092 QuoteTick, TradeTick,
3093 close::InstrumentClose,
3094 greeks::OptionGreekValues,
3095 option_chain::{OptionChainSlice, OptionGreeks},
3096 stubs::{stub_custom_data, stub_deltas, stub_depth10},
3097 },
3098 enums::{
3099 AggressorSide, BookType, GreeksConvention, InstrumentCloseType, MarketStatusAction,
3100 },
3101 identifiers::{ActorId, ClientId, ComponentId, OptionSeriesId, TradeId, TraderId, Venue},
3102 instruments::{CurrencyPair, InstrumentAny, stubs::audusd_sim},
3103 orderbook::OrderBook,
3104 types::{Price, Quantity},
3105 };
3106 use pyo3::{
3107 Bound, IntoPyObjectExt, Py, PyAny, PyRef, PyResult, Python,
3108 ffi::c_str,
3109 types::{PyAnyMethods, PyBytes, PyDict, PyList, PyWeakrefMethods, PyWeakrefReference},
3110 };
3111 use rstest::{fixture, rstest};
3112 use ustr::Ustr;
3113
3114 use super::{PyDataActor, prepare_python_actor};
3115 use crate::{
3116 actor::{DataActor, data_actor::DataActorConfig, registry::actor_exists},
3117 cache::Cache,
3118 clock::VirtualClock,
3119 component::{Component, get_component},
3120 enums::ComponentState,
3121 live::runner::replace_system_command_sender,
3122 messages::{
3123 SystemCommand,
3124 data::{BarsResponse, CustomDataResponse, QuotesResponse, TradesResponse},
3125 system::{
3126 QueueCondition, QueueState, QueueStateChanged, SocketState, SocketStateChanged,
3127 },
3128 },
3129 msgbus::{self, MessageBus, MessagingSwitchboard, get_message_bus},
3130 python::wrappers::get_python_wrapper,
3131 runner::{SyncDataCommandSender, SystemChannel, set_data_cmd_sender},
3132 signal::Signal,
3133 timer::TimeEvent,
3134 };
3135
3136 #[fixture]
3137 fn clock() -> Rc<RefCell<VirtualClock>> {
3138 Rc::new(RefCell::new(VirtualClock::new()))
3139 }
3140
3141 #[fixture]
3142 fn cache() -> Rc<RefCell<Cache>> {
3143 Rc::new(RefCell::new(Cache::new(None, None)))
3144 }
3145
3146 #[fixture]
3147 fn trader_id() -> TraderId {
3148 TraderId::from("TRADER-001")
3149 }
3150
3151 #[fixture]
3152 fn client_id() -> ClientId {
3153 ClientId::new("TestClient")
3154 }
3155
3156 #[fixture]
3157 fn venue() -> Venue {
3158 Venue::from("SIM")
3159 }
3160
3161 #[fixture]
3162 fn data_type() -> DataType {
3163 DataType::new("TestData", None, None)
3164 }
3165
3166 #[fixture]
3167 fn bar_type(audusd_sim: CurrencyPair) -> BarType {
3168 BarType::from_str(&format!("{}-1-MINUTE-LAST-INTERNAL", audusd_sim.id)).unwrap()
3169 }
3170
3171 fn create_unregistered_actor() -> PyDataActor {
3172 PyDataActor::new(None)
3173 }
3174
3175 fn create_registered_actor(
3176 clock: Rc<RefCell<VirtualClock>>,
3177 cache: Rc<RefCell<Cache>>,
3178 trader_id: TraderId,
3179 ) -> PyDataActor {
3180 let sender = SyncDataCommandSender;
3182 set_data_cmd_sender(Arc::new(sender));
3183
3184 let mut actor = PyDataActor::new(None);
3185 actor.register(trader_id, clock, cache).unwrap();
3186 actor
3187 }
3188
3189 #[rstest]
3190 fn test_new_actor_creation() {
3191 let actor = PyDataActor::new(None);
3192 assert!(actor.trader_id().is_none());
3193 }
3194
3195 #[rstest]
3196 fn test_actor_retains_python_config_object() {
3197 pyo3::Python::initialize();
3198 Python::attach(|py| {
3199 let config = py
3200 .eval(
3201 c_str!("type('_Cfg', (), {'actor_id': 'A-RETAIN-001'})()"),
3202 None,
3203 None,
3204 )
3205 .unwrap();
3206
3207 let actor = py
3208 .get_type::<PyDataActor>()
3209 .as_any()
3210 .call1((config.clone(),))
3211 .unwrap();
3212
3213 let retained = actor.getattr("config").unwrap();
3214
3215 assert!(retained.is(&config));
3216 });
3217 }
3218
3219 #[rstest]
3220 fn test_prepare_python_actor_applies_shared_registration_boundary() {
3221 pyo3::Python::initialize();
3222
3223 Python::attach(|py| {
3224 let namespace = PyDict::new(py);
3225 namespace
3226 .set_item("DataActor", py.get_type::<PyDataActor>())
3227 .unwrap();
3228 py.run(
3229 c_str!(
3230 r#"
3231class PreparedActor(DataActor):
3232 def __init__(self, _config=None):
3233 pass
3234"#
3235 ),
3236 Some(&namespace),
3237 None,
3238 )
3239 .unwrap();
3240 let actor_class = namespace.get_item("PreparedActor").unwrap();
3241
3242 let configured = py
3243 .eval(
3244 c_str!(
3245 "type('_Cfg', (), {'actor_id': 'CONFIGURED-001', 'log_events': False, 'log_commands': False})()"
3246 ),
3247 None,
3248 None,
3249 )
3250 .unwrap();
3251 let configured_actor = actor_class.call1((configured.clone(),)).unwrap();
3252 let configured_id = prepare_python_actor(&configured_actor, Some(&configured)).unwrap();
3253 let configured_ref = configured_actor.extract::<PyRef<PyDataActor>>().unwrap();
3254 let configured_wrapper = configured_ref.inner().python_instance().unwrap().unwrap();
3255
3256 let fallback_actor = actor_class.call0().unwrap();
3257 let fallback_id = prepare_python_actor(&fallback_actor, None).unwrap();
3258 let fallback_ref = fallback_actor.extract::<PyRef<PyDataActor>>().unwrap();
3259
3260 let invalid_logging = py
3261 .eval(
3262 c_str!(
3263 "type('_Cfg', (), {'actor_id': None, 'log_events': object(), 'log_commands': object()})()"
3264 ),
3265 None,
3266 None,
3267 )
3268 .unwrap();
3269 let invalid_logging_actor = actor_class.call1((invalid_logging.clone(),)).unwrap();
3270 let invalid_logging_id =
3271 prepare_python_actor(&invalid_logging_actor, Some(&invalid_logging)).unwrap();
3272 let invalid_logging_ref = invalid_logging_actor
3273 .extract::<PyRef<PyDataActor>>()
3274 .unwrap();
3275
3276 let invalid = py
3277 .eval(
3278 c_str!("type('_Cfg', (), {'actor_id': object()})()"),
3279 None,
3280 None,
3281 )
3282 .unwrap();
3283 let invalid_actor = actor_class.call1((invalid.clone(),)).unwrap();
3284 let error = prepare_python_actor(&invalid_actor, Some(&invalid)).unwrap_err();
3285
3286 assert_eq!(configured_id, ActorId::from("CONFIGURED-001"));
3287 assert_eq!(configured_ref.config.actor_id, Some(configured_id));
3288 assert!(!configured_ref.config.log_events);
3289 assert!(!configured_ref.config.log_commands);
3290 assert!(configured_wrapper.bind(py).is(&configured_actor));
3291 assert_eq!(fallback_id, ActorId::from("PreparedActor"));
3292 assert_eq!(fallback_ref.config.actor_id, Some(fallback_id));
3293 assert_eq!(invalid_logging_id, ActorId::from("PreparedActor"));
3294 assert!(invalid_logging_ref.config.log_events);
3295 assert!(invalid_logging_ref.config.log_commands);
3296 assert_eq!(error.to_string(), "Invalid `actor_id` type");
3297 });
3298 }
3299
3300 #[rstest]
3301 fn test_clock_access_before_registration_raises_error() {
3302 let actor = PyDataActor::new(None);
3303
3304 let result = actor.py_clock();
3306 assert!(result.is_err());
3307
3308 let error = result.unwrap_err();
3309 pyo3::Python::initialize();
3310
3311 pyo3::Python::attach(|py| {
3312 assert!(error.is_instance_of::<pyo3::exceptions::PyRuntimeError>(py));
3313 });
3314
3315 let error_msg = error.to_string();
3316 assert!(
3317 error_msg.contains("Actor must be registered with a trader before accessing clock")
3318 );
3319 }
3320
3321 #[rstest]
3322 fn test_unregistered_actor_methods_work() {
3323 let actor = create_unregistered_actor();
3324
3325 assert!(!actor.py_is_ready());
3326 assert!(!actor.py_is_running());
3327 assert!(!actor.py_is_stopped());
3328 assert!(!actor.py_is_disposed());
3329 assert!(!actor.py_is_degraded());
3330 assert!(!actor.py_is_faulted());
3331
3332 assert_eq!(actor.trader_id(), None);
3334 }
3335
3336 #[rstest]
3337 fn test_registration_success(
3338 clock: Rc<RefCell<VirtualClock>>,
3339 cache: Rc<RefCell<Cache>>,
3340 trader_id: TraderId,
3341 ) {
3342 let mut actor = create_unregistered_actor();
3343 actor.register(trader_id, clock, cache).unwrap();
3344 assert!(actor.trader_id().is_some());
3345 assert_eq!(actor.trader_id().unwrap(), trader_id);
3346 }
3347
3348 #[rstest]
3349 fn test_registered_actor_basic_properties(
3350 clock: Rc<RefCell<VirtualClock>>,
3351 cache: Rc<RefCell<Cache>>,
3352 trader_id: TraderId,
3353 ) {
3354 let actor = create_registered_actor(clock, cache, trader_id);
3355
3356 assert_eq!(actor.state(), ComponentState::Ready);
3357 assert_eq!(actor.trader_id(), Some(TraderId::from("TRADER-001")));
3358 assert!(actor.py_is_ready());
3359 assert!(!actor.py_is_running());
3360 assert!(!actor.py_is_stopped());
3361 assert!(!actor.py_is_disposed());
3362 assert!(!actor.py_is_degraded());
3363 assert!(!actor.py_is_faulted());
3364 }
3365
3366 #[rstest]
3367 fn test_basic_subscription_methods_compile(
3368 clock: Rc<RefCell<VirtualClock>>,
3369 cache: Rc<RefCell<Cache>>,
3370 trader_id: TraderId,
3371 data_type: DataType,
3372 client_id: ClientId,
3373 audusd_sim: CurrencyPair,
3374 ) {
3375 let mut actor = create_registered_actor(clock, cache, trader_id);
3376
3377 pyo3::Python::initialize();
3378
3379 pyo3::Python::attach(|py| {
3380 assert!(
3381 actor
3382 .py_subscribe_data(py, data_type.clone(), Some(client_id), None)
3383 .is_ok()
3384 );
3385 assert!(
3386 actor
3387 .py_subscribe_quotes(py, audusd_sim.id, Some(client_id), None)
3388 .is_ok()
3389 );
3390 assert!(
3391 actor
3392 .py_unsubscribe_data(py, data_type, Some(client_id), None)
3393 .is_ok()
3394 );
3395 assert!(
3396 actor
3397 .py_unsubscribe_quotes(py, audusd_sim.id, Some(client_id), None)
3398 .is_ok()
3399 );
3400 });
3401 }
3402
3403 #[rstest]
3404 fn test_shutdown_system_passes_through(
3405 clock: Rc<RefCell<VirtualClock>>,
3406 cache: Rc<RefCell<Cache>>,
3407 trader_id: TraderId,
3408 ) {
3409 let actor = create_registered_actor(clock, cache, trader_id);
3410
3411 assert!(
3412 actor
3413 .py_shutdown_system(Some("Test shutdown".to_string()))
3414 .is_ok()
3415 );
3416 assert!(actor.py_shutdown_system(None).is_ok());
3417 }
3418
3419 #[rstest]
3420 fn test_publish_data_delivers_to_any_subscriber(
3421 clock: Rc<RefCell<VirtualClock>>,
3422 cache: Rc<RefCell<Cache>>,
3423 trader_id: TraderId,
3424 ) {
3425 use crate::msgbus::{
3426 self, MessageBus, get_message_bus, switchboard::get_custom_topic,
3427 typed_handler::ShareableMessageHandler,
3428 };
3429
3430 *get_message_bus().borrow_mut() = MessageBus::default();
3432
3433 let actor = create_registered_actor(clock, cache, trader_id);
3434 let data = stub_custom_data(1, 42, None, None);
3435 let topic = get_custom_topic(&data.data_type);
3436
3437 let received: Rc<RefCell<Vec<CustomData>>> = Rc::new(RefCell::new(Vec::new()));
3438 let received_clone = received.clone();
3439 let handler = ShareableMessageHandler::from_typed(move |d: &CustomData| {
3440 received_clone.borrow_mut().push(d.clone());
3441 });
3442 msgbus::subscribe_any(topic.into(), handler, None);
3443
3444 actor.py_publish_data(&data.data_type, &data).unwrap();
3445
3446 let received = received.borrow();
3447 assert_eq!(received.len(), 1);
3448 assert_eq!(received[0].data_type, data.data_type);
3449 }
3450
3451 #[rstest]
3452 fn test_publish_signal_delivers_to_customdata_subscriber(
3453 clock: Rc<RefCell<VirtualClock>>,
3454 cache: Rc<RefCell<Cache>>,
3455 trader_id: TraderId,
3456 ) {
3457 use crate::{
3458 msgbus::{
3459 self, MessageBus, Pattern, get_message_bus, typed_handler::ShareableMessageHandler,
3460 },
3461 signal::Signal,
3462 };
3463
3464 *get_message_bus().borrow_mut() = MessageBus::default();
3465
3466 let actor = create_registered_actor(clock, cache, trader_id);
3467
3468 let received: Rc<RefCell<Vec<Signal>>> = Rc::new(RefCell::new(Vec::new()));
3471 let received_clone = received.clone();
3472 let handler = ShareableMessageHandler::from_typed(move |data: &CustomData| {
3473 if let Some(sig) = data.data.as_any().downcast_ref::<Signal>() {
3474 received_clone.borrow_mut().push(sig.clone());
3475 }
3476 });
3477 let pattern: crate::msgbus::MStr<Pattern> = "data.Signal*".to_string().into();
3478 msgbus::subscribe_any(pattern, handler, None);
3479
3480 pyo3::Python::initialize();
3481
3482 Python::attach(|py| {
3483 let val1: Py<PyAny> = 1.0_f64.into_py_any_unwrap(py);
3484 let val2: Py<PyAny> = "HIGH".into_py_any_unwrap(py);
3485 actor.py_publish_signal(py, "example", val1, 0).unwrap();
3486 actor
3487 .py_publish_signal(py, "risk", val2, 1_700_000_000_000_000_000)
3488 .unwrap();
3489 });
3490
3491 let received = received.borrow();
3492 assert_eq!(received.len(), 2);
3493 assert_eq!(received[0].name, "example");
3494 assert_eq!(received[0].value, "1.0");
3495 assert_eq!(received[1].name, "risk");
3496 assert_eq!(received[1].value, "HIGH");
3497 assert_eq!(
3498 received[1].ts_event,
3499 UnixNanos::from(1_700_000_000_000_000_000_u64)
3500 );
3501 }
3502
3503 #[rstest]
3504 fn test_publish_signal_accepts_numeric_py_values(
3505 clock: Rc<RefCell<VirtualClock>>,
3506 cache: Rc<RefCell<Cache>>,
3507 trader_id: TraderId,
3508 ) {
3509 use crate::{
3510 msgbus::{
3511 self, MessageBus, Pattern, get_message_bus, typed_handler::ShareableMessageHandler,
3512 },
3513 signal::Signal,
3514 };
3515
3516 *get_message_bus().borrow_mut() = MessageBus::default();
3517
3518 let actor = create_registered_actor(clock, cache, trader_id);
3519
3520 let received: Rc<RefCell<Vec<Signal>>> = Rc::new(RefCell::new(Vec::new()));
3521 let received_clone = received.clone();
3522 let handler = ShareableMessageHandler::from_typed(move |data: &CustomData| {
3523 if let Some(sig) = data.data.as_any().downcast_ref::<Signal>() {
3524 received_clone.borrow_mut().push(sig.clone());
3525 }
3526 });
3527 let pattern: crate::msgbus::MStr<Pattern> = "data.Signal*".to_string().into();
3528 msgbus::subscribe_any(pattern, handler, None);
3529
3530 pyo3::Python::initialize();
3531
3532 Python::attach(|py| {
3533 let int_value: Py<PyAny> = 42_i64.into_py_any_unwrap(py);
3534 let float_value: Py<PyAny> = 3.5_f64.into_py_any_unwrap(py);
3535 let bool_value: Py<PyAny> = true.into_py_any_unwrap(py);
3536 actor.py_publish_signal(py, "count", int_value, 0).unwrap();
3537 actor
3538 .py_publish_signal(py, "ratio", float_value, 0)
3539 .unwrap();
3540 actor
3541 .py_publish_signal(py, "active", bool_value, 0)
3542 .unwrap();
3543 });
3544
3545 let received = received.borrow();
3546 assert_eq!(received.len(), 3);
3547 assert_eq!(received[0].value, "42");
3548 assert_eq!(received[1].value, "3.5");
3549 assert_eq!(received[2].value, "True");
3550 }
3551
3552 #[rstest]
3553 fn test_subscribe_and_unsubscribe_signal_compile(
3554 clock: Rc<RefCell<VirtualClock>>,
3555 cache: Rc<RefCell<Cache>>,
3556 trader_id: TraderId,
3557 ) {
3558 use crate::msgbus::{MessageBus, get_message_bus};
3559
3560 *get_message_bus().borrow_mut() = MessageBus::default();
3561
3562 let actor = create_registered_actor(clock, cache, trader_id);
3563 Python::initialize();
3564 Python::attach(|py| {
3565 let actor = Bound::new(py, actor).unwrap();
3566 PyDataActor::py_subscribe_signal(&actor, "example", None).unwrap();
3567 PyDataActor::py_unsubscribe_signal(&actor, "example").unwrap();
3568 PyDataActor::py_subscribe_signal(&actor, "", None).unwrap();
3569 PyDataActor::py_unsubscribe_signal(&actor, "").unwrap();
3570 });
3571 }
3572
3573 #[rstest]
3574 fn test_py_subscribe_signal_forwards_priority(
3575 clock: Rc<RefCell<VirtualClock>>,
3576 cache: Rc<RefCell<Cache>>,
3577 trader_id: TraderId,
3578 ) {
3579 use crate::msgbus::{MessageBus, get_message_bus, switchboard::get_signal_topic};
3580
3581 *get_message_bus().borrow_mut() = MessageBus::default();
3582
3583 let actor = create_registered_actor(clock, cache, trader_id);
3584 Python::initialize();
3585 Python::attach(|py| {
3586 let actor = Bound::new(py, actor).unwrap();
3587 PyDataActor::py_subscribe_signal(&actor, "trigger", Some(50)).unwrap();
3588
3589 let topic = get_signal_topic("trigger");
3591 let subs = get_message_bus().borrow_mut().matching_subscriptions(topic);
3592 assert_eq!(subs.len(), 1);
3593 assert_eq!(subs[0].priority, 50);
3594 });
3595 }
3596
3597 #[rstest]
3598 fn test_register_in_global_registries_retains_python_wrapper(
3599 clock: Rc<RefCell<VirtualClock>>,
3600 cache: Rc<RefCell<Cache>>,
3601 trader_id: TraderId,
3602 ) {
3603 pyo3::Python::initialize();
3604
3605 Python::attach(|py| {
3606 let py_actor = create_tracking_python_actor(py).unwrap();
3607
3608 let mut rust_actor = PyDataActor::new(Some(DataActorConfig {
3609 actor_id: Some(ActorId::from("RETAINED-ACTOR")),
3610 ..Default::default()
3611 }));
3612 rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
3613 rust_actor.register(trader_id, clock, cache).unwrap();
3614
3615 rust_actor.register_in_global_registries().unwrap();
3616
3617 let retained = get_python_wrapper(ComponentId::from("RETAINED-ACTOR"))
3618 .expect("registering must retain the actor's Python wrapper");
3619
3620 assert!(retained.bind(py).is(py_actor.bind(py)));
3621 assert!(get_component(&Ustr::from("RETAINED-ACTOR")).is_some());
3622 assert!(actor_exists(&Ustr::from("RETAINED-ACTOR")));
3623 });
3624 }
3625
3626 #[rstest]
3627 fn test_register_in_global_registries_rejects_missing_python_wrapper(
3628 clock: Rc<RefCell<VirtualClock>>,
3629 cache: Rc<RefCell<Cache>>,
3630 trader_id: TraderId,
3631 ) {
3632 pyo3::Python::initialize();
3633
3634 Python::attach(|_py| {
3635 let mut rust_actor = PyDataActor::new(Some(DataActorConfig {
3636 actor_id: Some(ActorId::from("UNWRAPPED-ACTOR")),
3637 ..Default::default()
3638 }));
3639 rust_actor.register(trader_id, clock, cache).unwrap();
3640
3641 let error = rust_actor
3642 .register_in_global_registries()
3643 .expect_err("registering without a Python wrapper must fail");
3644
3645 assert!(error.to_string().contains("without a Python wrapper"));
3646 assert!(get_component(&Ustr::from("UNWRAPPED-ACTOR")).is_none());
3647 assert!(!actor_exists(&Ustr::from("UNWRAPPED-ACTOR")));
3648 assert!(get_python_wrapper(ComponentId::from("UNWRAPPED-ACTOR")).is_none());
3649 });
3650 }
3651
3652 #[rstest]
3653 fn test_publish_data_dispatches_to_python_on_data(
3654 clock: Rc<RefCell<VirtualClock>>,
3655 cache: Rc<RefCell<Cache>>,
3656 trader_id: TraderId,
3657 ) {
3658 use crate::msgbus::{MessageBus, get_message_bus};
3659
3660 *get_message_bus().borrow_mut() = MessageBus::default();
3661
3662 pyo3::Python::initialize();
3663
3664 Python::attach(|py| {
3665 let py_actor = create_tracking_python_actor(py).unwrap();
3666
3667 let mut rust_actor = PyDataActor::new(None);
3668 rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
3669 rust_actor.register(trader_id, clock, cache).unwrap();
3670 rust_actor.register_in_global_registries().unwrap();
3671 rust_actor.py_start().unwrap();
3672
3673 let data = stub_custom_data(1, 42, None, None);
3674 rust_actor
3675 .py_subscribe_data(py, data.data_type.clone(), None, None)
3676 .unwrap();
3677
3678 rust_actor.py_publish_data(&data.data_type, &data).unwrap();
3679 rust_actor.py_publish_data(&data.data_type, &data).unwrap();
3680
3681 assert!(python_method_was_called(&py_actor, py, "on_data"));
3682 assert_eq!(python_method_call_count(&py_actor, py, "on_data"), 2);
3683 });
3684 }
3685
3686 #[rstest]
3687 fn test_publish_signal_dispatches_to_python_on_signal(
3688 clock: Rc<RefCell<VirtualClock>>,
3689 cache: Rc<RefCell<Cache>>,
3690 trader_id: TraderId,
3691 ) {
3692 use crate::msgbus::{MessageBus, get_message_bus};
3693
3694 *get_message_bus().borrow_mut() = MessageBus::default();
3695
3696 pyo3::Python::initialize();
3697
3698 Python::attach(|py| {
3699 let py_actor = create_tracking_python_actor(py).unwrap();
3700
3701 let mut rust_actor = PyDataActor::new(None);
3702 rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
3703 rust_actor.register(trader_id, clock, cache).unwrap();
3704 rust_actor.register_in_global_registries().unwrap();
3705 rust_actor.py_start().unwrap();
3706
3707 let bound_actor = Bound::new(py, rust_actor).unwrap();
3708 PyDataActor::py_subscribe_signal(&bound_actor, "example", None).unwrap();
3709 let rust_actor = bound_actor.borrow_mut();
3710 let val1: Py<PyAny> = "1.5".into_py_any_unwrap(py);
3711 let val2: Py<PyAny> = 2.0_f64.into_py_any_unwrap(py);
3712 rust_actor
3713 .py_publish_signal(py, "example", val1, 0)
3714 .unwrap();
3715 rust_actor
3716 .py_publish_signal(py, "example", val2, 1_700_000_000_000_000_000)
3717 .unwrap();
3718
3719 assert!(python_method_was_called(&py_actor, py, "on_signal"));
3720 assert_eq!(python_method_call_count(&py_actor, py, "on_signal"), 2);
3721 });
3722 }
3723
3724 #[rstest]
3725 fn test_unsubscribe_signal_stops_python_dispatch(
3726 clock: Rc<RefCell<VirtualClock>>,
3727 cache: Rc<RefCell<Cache>>,
3728 trader_id: TraderId,
3729 ) {
3730 use crate::msgbus::{MessageBus, get_message_bus};
3731
3732 *get_message_bus().borrow_mut() = MessageBus::default();
3733
3734 pyo3::Python::initialize();
3735
3736 Python::attach(|py| {
3737 let py_actor = create_tracking_python_actor(py).unwrap();
3738
3739 let mut rust_actor = PyDataActor::new(None);
3740 rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
3741 rust_actor.register(trader_id, clock, cache).unwrap();
3742 rust_actor.register_in_global_registries().unwrap();
3743 rust_actor.py_start().unwrap();
3744
3745 let bound_actor = Bound::new(py, rust_actor).unwrap();
3746 PyDataActor::py_subscribe_signal(&bound_actor, "example", None).unwrap();
3747 let rust_actor = bound_actor.borrow_mut();
3748 let val1: Py<PyAny> = "1".into_py_any_unwrap(py);
3749 let val2: Py<PyAny> = "2".into_py_any_unwrap(py);
3750 rust_actor
3751 .py_publish_signal(py, "example", val1, 0)
3752 .unwrap();
3753
3754 drop(rust_actor);
3755 PyDataActor::py_unsubscribe_signal(&bound_actor, "example").unwrap();
3756 let rust_actor = bound_actor.borrow_mut();
3757 rust_actor
3758 .py_publish_signal(py, "example", val2, 0)
3759 .unwrap();
3760
3761 assert_eq!(python_method_call_count(&py_actor, py, "on_signal"), 1);
3762 });
3763 }
3764
3765 #[rstest]
3766 #[case(None)]
3767 #[case(Some(SystemChannel::ExecCommands))]
3768 fn test_queue_state_changed_subscription_dispatches_and_unsubscribes(
3769 clock: Rc<RefCell<VirtualClock>>,
3770 cache: Rc<RefCell<Cache>>,
3771 trader_id: TraderId,
3772 #[case] channel: Option<SystemChannel>,
3773 ) {
3774 *get_message_bus().borrow_mut() = MessageBus::default();
3775
3776 pyo3::Python::initialize();
3777
3778 Python::attach(|py| {
3779 let py_actor = create_tracking_python_actor(py).unwrap();
3780
3781 let mut rust_actor = PyDataActor::new(None);
3782 rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
3783 rust_actor.register(trader_id, clock, cache).unwrap();
3784 rust_actor.register_in_global_registries().unwrap();
3785 rust_actor.py_start().unwrap();
3786 rust_actor
3787 .py_subscribe_queue_state(channel, Some(50))
3788 .unwrap();
3789
3790 let topic =
3791 MessagingSwitchboard::queue_state_changed_topic(SystemChannel::ExecCommands);
3792 let subscriptions = get_message_bus().borrow_mut().matching_subscriptions(topic);
3793 assert_eq!(subscriptions.len(), 1);
3794 assert_eq!(subscriptions[0].priority, 50);
3795 let unrelated =
3796 MessagingSwitchboard::queue_state_changed_topic(SystemChannel::DataEvents);
3797 assert_eq!(
3798 get_message_bus()
3799 .borrow_mut()
3800 .matching_subscriptions(unrelated)
3801 .len(),
3802 usize::from(channel.is_none())
3803 );
3804
3805 let triggered = sample_queue_state_changed(QueueState::Triggered);
3806 msgbus::publish_any(topic, &triggered);
3807
3808 let (received,) = py_actor
3809 .call_method1(py, "last_call_args", ("on_queue_state",))
3810 .unwrap()
3811 .extract::<(QueueStateChanged,)>(py)
3812 .unwrap();
3813 assert_eq!(received, triggered);
3814
3815 rust_actor.py_unsubscribe_queue_state(channel).unwrap();
3816 let cleared = sample_queue_state_changed(QueueState::Cleared);
3817 msgbus::publish_any(topic, &cleared);
3818
3819 assert_eq!(python_method_call_count(&py_actor, py, "on_queue_state"), 1);
3820 });
3821 }
3822
3823 #[rstest]
3824 #[case(None, None)]
3825 #[case(Some(ClientId::from("BINANCE")), None)]
3826 #[case(None, Some("binance-futures-market-streams"))]
3827 #[case(
3828 Some(ClientId::from("BINANCE")),
3829 Some("binance-futures-market-streams")
3830 )]
3831 fn test_socket_state_changed_subscription_dispatches_and_unsubscribes(
3832 clock: Rc<RefCell<VirtualClock>>,
3833 cache: Rc<RefCell<Cache>>,
3834 trader_id: TraderId,
3835 #[case] client_id: Option<ClientId>,
3836 #[case] endpoint: Option<&str>,
3837 ) {
3838 *get_message_bus().borrow_mut() = MessageBus::default();
3839
3840 pyo3::Python::initialize();
3841
3842 Python::attach(|py| {
3843 let py_actor = create_tracking_python_actor(py).unwrap();
3844
3845 let mut rust_actor = PyDataActor::new(None);
3846 rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
3847 rust_actor.register(trader_id, clock, cache).unwrap();
3848 rust_actor.register_in_global_registries().unwrap();
3849 rust_actor.py_start().unwrap();
3850 rust_actor
3851 .py_subscribe_socket_state(client_id, endpoint, Some(50))
3852 .unwrap();
3853
3854 let topic = MessagingSwitchboard::socket_state_changed_topic(
3855 ClientId::from("BINANCE"),
3856 "binance-futures-market-streams",
3857 );
3858 let subscriptions = get_message_bus().borrow_mut().matching_subscriptions(topic);
3859 assert_eq!(subscriptions.len(), 1);
3860 assert_eq!(subscriptions[0].priority, 50);
3861 let unrelated =
3862 MessagingSwitchboard::socket_state_changed_topic(ClientId::from("BYBIT"), "orders");
3863 assert_eq!(
3864 get_message_bus()
3865 .borrow_mut()
3866 .matching_subscriptions(unrelated)
3867 .len(),
3868 usize::from(client_id.is_none() && endpoint.is_none())
3869 );
3870
3871 let connected = sample_socket_state_changed(SocketState::Connected);
3872 msgbus::publish_any(topic, &connected);
3873
3874 let (received,) = py_actor
3875 .call_method1(py, "last_call_args", ("on_socket_state",))
3876 .unwrap()
3877 .extract::<(SocketStateChanged,)>(py)
3878 .unwrap();
3879 assert_eq!(received, connected);
3880
3881 rust_actor
3882 .py_unsubscribe_socket_state(client_id, endpoint)
3883 .unwrap();
3884 let disconnected = sample_socket_state_changed(SocketState::Disconnected);
3885 msgbus::publish_any(topic, &disconnected);
3886
3887 assert_eq!(
3888 python_method_call_count(&py_actor, py, "on_socket_state"),
3889 1
3890 );
3891 });
3892 }
3893
3894 #[rstest]
3895 fn test_subscribe_signal_wildcard_dispatches_all_names_to_python(
3896 clock: Rc<RefCell<VirtualClock>>,
3897 cache: Rc<RefCell<Cache>>,
3898 trader_id: TraderId,
3899 ) {
3900 use crate::msgbus::{MessageBus, get_message_bus};
3901
3902 *get_message_bus().borrow_mut() = MessageBus::default();
3903
3904 pyo3::Python::initialize();
3905
3906 Python::attach(|py| {
3907 let py_actor = create_tracking_python_actor(py).unwrap();
3908
3909 let mut rust_actor = PyDataActor::new(None);
3910 rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
3911 rust_actor.register(trader_id, clock, cache).unwrap();
3912 rust_actor.register_in_global_registries().unwrap();
3913 rust_actor.py_start().unwrap();
3914
3915 let bound_actor = Bound::new(py, rust_actor).unwrap();
3916 PyDataActor::py_subscribe_signal(&bound_actor, "", None).unwrap();
3917 let rust_actor = bound_actor.borrow_mut();
3918 let val1: Py<PyAny> = "1".into_py_any_unwrap(py);
3919 let val2: Py<PyAny> = "2".into_py_any_unwrap(py);
3920 let val3: Py<PyAny> = "3".into_py_any_unwrap(py);
3921 rust_actor.py_publish_signal(py, "alpha", val1, 0).unwrap();
3922 rust_actor.py_publish_signal(py, "beta", val2, 0).unwrap();
3923 rust_actor.py_publish_signal(py, "gamma", val3, 0).unwrap();
3924
3925 assert_eq!(python_method_call_count(&py_actor, py, "on_signal"), 3);
3926 });
3927 }
3928
3929 #[rstest]
3930 fn test_signal_customdata_unwraps_to_python_signal(
3931 clock: Rc<RefCell<VirtualClock>>,
3932 cache: Rc<RefCell<Cache>>,
3933 trader_id: TraderId,
3934 ) {
3935 use crate::msgbus::{MessageBus, get_message_bus};
3941
3942 *get_message_bus().borrow_mut() = MessageBus::default();
3943
3944 pyo3::Python::initialize();
3945
3946 Python::attach(|py| {
3947 let capture_code = c_str!(
3948 r#"
3949class CapturingActor:
3950 def __init__(self):
3951 self.captured = []
3952
3953 def on_start(self): pass
3954 def on_stop(self): pass
3955 def on_resume(self): pass
3956 def on_reset(self): pass
3957 def on_dispose(self): pass
3958 def on_degrade(self): pass
3959 def on_fault(self): pass
3960 def on_signal(self, signal): pass
3961
3962 def on_data(self, custom):
3963 # Exercise the CustomData.data getter: raises TypeError if the
3964 # inner payload cannot be converted back to a Python object.
3965 inner = custom.data
3966 self.captured.append((type(inner).__name__, inner.name, inner.value))
3967"#
3968 );
3969 py.run(capture_code, None, None).unwrap();
3970 let cls = py.eval(c_str!("CapturingActor"), None, None).unwrap();
3971 let py_actor: Py<PyAny> = cls.call0().unwrap().unbind();
3972
3973 let mut rust_actor = PyDataActor::new(None);
3974 rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
3975 rust_actor.register(trader_id, clock, cache).unwrap();
3976 rust_actor.register_in_global_registries().unwrap();
3977 rust_actor.py_start().unwrap();
3978
3979 let data_type = DataType::new("SignalExample", None, None);
3982 rust_actor
3983 .py_subscribe_data(py, data_type, None, None)
3984 .unwrap();
3985
3986 let val: Py<PyAny> = "1.5".into_py_any_unwrap(py);
3987 rust_actor.py_publish_signal(py, "example", val, 0).unwrap();
3988
3989 let captured = py_actor
3990 .bind(py)
3991 .getattr("captured")
3992 .unwrap()
3993 .extract::<Vec<(String, String, String)>>()
3994 .unwrap();
3995 assert_eq!(captured.len(), 1);
3996 assert_eq!(captured[0].0, "Signal");
3997 assert_eq!(captured[0].1, "example");
3998 assert_eq!(captured[0].2, "1.5");
3999 });
4000 }
4001
4002 #[rstest]
4003 fn test_add_and_update_synthetic_via_pyo3(
4004 clock: Rc<RefCell<VirtualClock>>,
4005 cache: Rc<RefCell<Cache>>,
4006 trader_id: TraderId,
4007 ) {
4008 use nautilus_model::{
4009 identifiers::{InstrumentId, Symbol},
4010 instruments::SyntheticInstrument,
4011 };
4012
4013 let actor = create_registered_actor(clock, cache.clone(), trader_id);
4014
4015 let comp1 = InstrumentId::from_str("BTC-USD.VENUE").unwrap();
4016 let comp2 = InstrumentId::from_str("ETH-USD.VENUE").unwrap();
4017 let formula = format!("({comp1} + {comp2}) / 2.0");
4018 let synthetic = SyntheticInstrument::builder()
4019 .symbol(Symbol::from("SYN"))
4020 .price_precision(2)
4021 .components(vec![comp1, comp2])
4022 .formula(&formula)
4023 .ts_event(UnixNanos::default())
4024 .ts_init(UnixNanos::default())
4025 .build()
4026 .unwrap();
4027 let synthetic_id = synthetic.id;
4028
4029 actor.py_add_synthetic(synthetic.clone()).unwrap();
4030 assert!(cache.borrow().synthetic(&synthetic_id).is_some());
4031
4032 assert!(actor.py_add_synthetic(synthetic).is_err());
4034
4035 let new_formula = format!("{comp1} + {comp2}");
4036 let updated = SyntheticInstrument::builder()
4037 .symbol(Symbol::from("SYN"))
4038 .price_precision(2)
4039 .components(vec![comp1, comp2])
4040 .formula(&new_formula)
4041 .ts_event(UnixNanos::default())
4042 .ts_init(UnixNanos::default())
4043 .build()
4044 .unwrap();
4045 actor.py_update_synthetic(updated).unwrap();
4046 assert_eq!(
4047 cache.borrow().synthetic(&synthetic_id).unwrap().formula,
4048 new_formula
4049 );
4050
4051 let missing = SyntheticInstrument::builder()
4053 .symbol(Symbol::from("GONE"))
4054 .price_precision(2)
4055 .components(vec![comp1, comp2])
4056 .formula(&formula)
4057 .ts_event(UnixNanos::default())
4058 .ts_init(UnixNanos::default())
4059 .build()
4060 .unwrap();
4061 assert!(actor.py_update_synthetic(missing).is_err());
4062 }
4063
4064 #[rstest]
4065 fn test_book_at_interval_invalid_interval_ms(
4066 clock: Rc<RefCell<VirtualClock>>,
4067 cache: Rc<RefCell<Cache>>,
4068 trader_id: TraderId,
4069 audusd_sim: CurrencyPair,
4070 ) {
4071 pyo3::Python::initialize();
4072
4073 let mut actor = create_registered_actor(clock, cache, trader_id);
4074
4075 pyo3::Python::attach(|py| {
4076 let result = actor.py_subscribe_book_at_interval(
4077 py,
4078 audusd_sim.id,
4079 BookType::L2_MBP,
4080 0,
4081 None,
4082 None,
4083 None,
4084 );
4085 assert!(result.is_err());
4086 assert_eq!(
4087 result.unwrap_err().to_string(),
4088 "ValueError: interval_ms must be > 0"
4089 );
4090
4091 let result = actor.py_unsubscribe_book_at_interval(py, audusd_sim.id, 0, None, None);
4092 assert!(result.is_err());
4093 assert_eq!(
4094 result.unwrap_err().to_string(),
4095 "ValueError: interval_ms must be > 0"
4096 );
4097 });
4098 }
4099
4100 #[rstest]
4101 #[case(None)]
4102 #[case(Some(25))]
4103 fn test_book_depth_subscription_methods_manage_handler(
4104 #[case] depth: Option<usize>,
4105 clock: Rc<RefCell<VirtualClock>>,
4106 cache: Rc<RefCell<Cache>>,
4107 trader_id: TraderId,
4108 audusd_sim: CurrencyPair,
4109 ) {
4110 pyo3::Python::initialize();
4111
4112 let mut actor = create_registered_actor(clock, cache, trader_id);
4113
4114 Python::attach(|py| {
4115 actor
4116 .py_subscribe_book_depth(
4117 py,
4118 audusd_sim.id,
4119 BookType::L2_MBP,
4120 depth,
4121 None,
4122 false,
4123 None,
4124 )
4125 .unwrap();
4126 assert_eq!(actor.inner().depth_handler_count(), 1);
4127
4128 actor
4129 .py_unsubscribe_book_depth(py, audusd_sim.id, None, None)
4130 .unwrap();
4131 assert_eq!(actor.inner().depth_handler_count(), 0);
4132 });
4133 }
4134
4135 #[rstest]
4136 fn test_request_methods_signatures_exist() {
4137 let actor = create_unregistered_actor();
4138 assert!(actor.trader_id().is_none());
4139 }
4140
4141 #[rstest]
4142 fn test_data_actor_trait_implementation(
4143 clock: Rc<RefCell<VirtualClock>>,
4144 cache: Rc<RefCell<Cache>>,
4145 trader_id: TraderId,
4146 ) {
4147 let actor = create_registered_actor(clock, cache, trader_id);
4148 let state = actor.state();
4149 assert_eq!(state, ComponentState::Ready);
4150 }
4151
4152 #[rstest]
4153 fn test_python_reconnect_socket_enqueues_typed_command(
4154 clock: Rc<RefCell<VirtualClock>>,
4155 cache: Rc<RefCell<Cache>>,
4156 trader_id: TraderId,
4157 ) {
4158 let (system_tx, mut system_rx) = tokio::sync::mpsc::unbounded_channel();
4159 replace_system_command_sender(system_tx);
4160 let actor = create_registered_actor(clock, cache, trader_id);
4161
4162 actor
4163 .py_reconnect_socket(ClientId::from("POLYMARKET"), "polymarket-market-streams")
4164 .expect("valid reconnect command");
4165 let command = system_rx
4166 .try_recv()
4167 .expect("reconnect command should be queued");
4168 let SystemCommand::ReconnectSocket(command) = command;
4169
4170 assert_eq!(command.trader_id, trader_id);
4171 assert_eq!(command.client_id, ClientId::from("POLYMARKET"));
4172 assert_eq!(command.endpoint, "polymarket-market-streams");
4173 assert_eq!(command.ts_init, UnixNanos::default());
4174 }
4175
4176 #[rstest]
4177 fn test_python_reconnect_socket_returns_errors_without_panicking() {
4178 let actor = create_unregistered_actor();
4179
4180 assert!(
4181 actor
4182 .py_reconnect_socket(ClientId::from("POLYMARKET"), "polymarket-market-streams")
4183 .is_err()
4184 );
4185 assert!(
4186 actor
4187 .py_reconnect_socket(ClientId::from("POLYMARKET"), "wss://secret.example")
4188 .is_err()
4189 );
4190 }
4191
4192 fn sample_instrument() -> CurrencyPair {
4193 audusd_sim()
4194 }
4195
4196 fn sample_data() -> CustomData {
4197 stub_custom_data(1, 42, None, None)
4198 }
4199
4200 fn sample_time_event() -> TimeEvent {
4201 TimeEvent::new(
4202 Ustr::from("test_timer"),
4203 UUID4::new(),
4204 UnixNanos::default(),
4205 UnixNanos::default(),
4206 )
4207 }
4208
4209 fn sample_signal() -> Signal {
4210 Signal::new(
4211 Ustr::from("test_signal"),
4212 "1.0".to_string(),
4213 UnixNanos::default(),
4214 UnixNanos::default(),
4215 )
4216 }
4217
4218 fn sample_queue_state_changed(state: QueueState) -> QueueStateChanged {
4219 QueueStateChanged::new(
4220 TraderId::from("TRADER-001"),
4221 SystemChannel::ExecCommands,
4222 QueueCondition::Backlogged,
4223 state,
4224 17,
4225 23,
4226 UUID4::from("00000000-0000-4000-8000-000000000001"),
4227 UnixNanos::from(1_700_000_000_000_000_001),
4228 UnixNanos::from(1_700_000_000_000_000_002),
4229 )
4230 }
4231
4232 fn sample_socket_state_changed(state: SocketState) -> SocketStateChanged {
4233 SocketStateChanged::new(
4234 TraderId::from("TRADER-001"),
4235 ClientId::from("BINANCE"),
4236 Some(Venue::from("BINANCE")),
4237 Ustr::from("binance-futures-market-streams"),
4238 state,
4239 UUID4::from("00000000-0000-4000-8000-000000000001"),
4240 UnixNanos::from(1_700_000_000_000_000_001),
4241 UnixNanos::from(1_700_000_000_000_000_002),
4242 )
4243 }
4244
4245 fn sample_quote() -> QuoteTick {
4246 let instrument = sample_instrument();
4247 QuoteTick::new(
4248 instrument.id,
4249 Price::from("1.00000"),
4250 Price::from("1.00001"),
4251 Quantity::from(100_000),
4252 Quantity::from(100_000),
4253 UnixNanos::default(),
4254 UnixNanos::default(),
4255 )
4256 }
4257
4258 fn sample_trade() -> TradeTick {
4259 let instrument = sample_instrument();
4260 TradeTick::new(
4261 instrument.id,
4262 Price::from("1.00000"),
4263 Quantity::from(100_000),
4264 AggressorSide::Buy,
4265 TradeId::new("123456"),
4266 UnixNanos::default(),
4267 UnixNanos::default(),
4268 )
4269 }
4270
4271 fn sample_bar() -> Bar {
4272 let instrument = sample_instrument();
4273 let bar_type =
4274 BarType::from_str(&format!("{}-1-MINUTE-LAST-INTERNAL", instrument.id)).unwrap();
4275 Bar::new(
4276 bar_type,
4277 Price::from("1.00000"),
4278 Price::from("1.00010"),
4279 Price::from("0.99990"),
4280 Price::from("1.00005"),
4281 Quantity::from(100_000),
4282 UnixNanos::default(),
4283 UnixNanos::default(),
4284 )
4285 }
4286
4287 fn sample_book() -> OrderBook {
4288 OrderBook::new(sample_instrument().id, BookType::L2_MBP)
4289 }
4290
4291 fn sample_book_deltas() -> OrderBookDeltas {
4292 let instrument = sample_instrument();
4293 let delta =
4294 OrderBookDelta::clear(instrument.id, 0, UnixNanos::default(), UnixNanos::default());
4295 OrderBookDeltas::new(instrument.id, vec![delta])
4296 }
4297
4298 fn sample_book_depth() -> OrderBookDepth {
4299 stub_depth10()
4300 }
4301
4302 fn sample_mark_price() -> MarkPriceUpdate {
4303 MarkPriceUpdate::new(
4304 sample_instrument().id,
4305 Price::from("1.00000"),
4306 UnixNanos::default(),
4307 UnixNanos::default(),
4308 )
4309 }
4310
4311 fn sample_index_price() -> IndexPriceUpdate {
4312 IndexPriceUpdate::new(
4313 sample_instrument().id,
4314 Price::from("1.00000"),
4315 UnixNanos::default(),
4316 UnixNanos::default(),
4317 )
4318 }
4319
4320 fn sample_funding_rate() -> FundingRateUpdate {
4321 FundingRateUpdate::new(
4322 sample_instrument().id,
4323 "0.0001".parse().unwrap(),
4324 None,
4325 None,
4326 UnixNanos::default(),
4327 UnixNanos::default(),
4328 )
4329 }
4330
4331 fn sample_instrument_status() -> InstrumentStatus {
4332 InstrumentStatus::new(
4333 sample_instrument().id,
4334 MarketStatusAction::Trading,
4335 UnixNanos::default(),
4336 UnixNanos::default(),
4337 None,
4338 None,
4339 None,
4340 None,
4341 None,
4342 )
4343 }
4344
4345 fn sample_instrument_close() -> InstrumentClose {
4346 InstrumentClose::new(
4347 sample_instrument().id,
4348 Price::from("1.00000"),
4349 InstrumentCloseType::EndOfSession,
4350 UnixNanos::default(),
4351 UnixNanos::default(),
4352 )
4353 }
4354
4355 fn sample_option_greeks() -> OptionGreeks {
4356 OptionGreeks {
4357 instrument_id: sample_instrument().id,
4358 convention: GreeksConvention::BlackScholes,
4359 greeks: OptionGreekValues {
4360 delta: 0.55,
4361 gamma: 0.03,
4362 vega: 0.12,
4363 theta: -0.05,
4364 rho: 0.01,
4365 },
4366 mark_iv: Some(0.25),
4367 bid_iv: None,
4368 ask_iv: None,
4369 underlying_price: None,
4370 open_interest: None,
4371 ts_event: UnixNanos::default(),
4372 ts_init: UnixNanos::default(),
4373 }
4374 }
4375
4376 fn sample_option_chain() -> OptionChainSlice {
4377 OptionChainSlice {
4378 series_id: OptionSeriesId::new_derived(
4379 Venue::from("SIM"),
4380 Ustr::from("AUD"),
4381 Ustr::from("USD"),
4382 UnixNanos::from(1_711_036_800_000_000_000),
4383 ),
4384 atm_strike: None,
4385 calls: Default::default(),
4386 puts: Default::default(),
4387 ts_event: UnixNanos::default(),
4388 ts_init: UnixNanos::default(),
4389 }
4390 }
4391
4392 #[cfg(feature = "defi")]
4393 fn sample_block() -> Block {
4394 Block::new(
4395 "0x1234567890abcdef".to_string(),
4396 "0xabcdef1234567890".to_string(),
4397 12345,
4398 "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0".into(),
4399 21000,
4400 20000,
4401 UnixNanos::default(),
4402 Some(Blockchain::Ethereum),
4403 )
4404 }
4405
4406 #[cfg(feature = "defi")]
4407 fn sample_pool_components() -> (Arc<Chain>, Arc<Dex>, Pool) {
4408 let chain = Arc::new(Chain::new(Blockchain::Ethereum, 1));
4409 let dex = Arc::new(Dex::new(
4410 Chain::new(Blockchain::Ethereum, 1),
4411 DexType::UniswapV3,
4412 "0x1F98431c8aD98523631AE4a59f267346ea31F984",
4413 0,
4414 AmmType::CLAMM,
4415 "PoolCreated",
4416 "Swap",
4417 "Mint",
4418 "Burn",
4419 "Collect",
4420 ));
4421 let token0 = Token::new(
4422 chain.clone(),
4423 "0xa0b86a33e6441c8c06dd7b111a8c4e82e2b2a5e1"
4424 .parse()
4425 .unwrap(),
4426 "USDC".into(),
4427 "USD Coin".into(),
4428 6,
4429 );
4430 let token1 = Token::new(
4431 chain.clone(),
4432 "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
4433 .parse()
4434 .unwrap(),
4435 "WETH".into(),
4436 "Wrapped Ether".into(),
4437 18,
4438 );
4439 let pool_address = "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8"
4440 .parse()
4441 .unwrap();
4442 let pool_identifier: PoolIdentifier = "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8"
4443 .parse()
4444 .unwrap();
4445
4446 let pool = Pool::new(
4447 chain.clone(),
4448 dex.clone(),
4449 pool_address,
4450 pool_identifier,
4451 12345,
4452 token0,
4453 token1,
4454 Some(500),
4455 Some(10),
4456 UnixNanos::default(),
4457 );
4458
4459 (chain, dex, pool)
4460 }
4461
4462 #[cfg(feature = "defi")]
4463 fn sample_pool_swap() -> PoolSwap {
4464 let (chain, dex, pool) = sample_pool_components();
4465 PoolSwap::new(
4466 chain,
4467 dex,
4468 pool.instrument_id,
4469 pool.pool_identifier,
4470 12345,
4471 "0xabc123".to_string(),
4472 0,
4473 0,
4474 UnixNanos::default(),
4475 UnixNanos::default(),
4476 "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
4477 .parse()
4478 .unwrap(),
4479 "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
4480 .parse()
4481 .unwrap(),
4482 I256::from_str("1000000000000000000").unwrap(),
4483 I256::from_str("400000000000000").unwrap(),
4484 U160::from(59000000000000u128),
4485 1000000,
4486 100,
4487 )
4488 }
4489
4490 #[cfg(feature = "defi")]
4491 fn sample_pool_liquidity_update() -> PoolLiquidityUpdate {
4492 let (chain, dex, pool) = sample_pool_components();
4493 PoolLiquidityUpdate::new(
4494 chain,
4495 dex,
4496 pool.instrument_id,
4497 pool.pool_identifier,
4498 PoolLiquidityUpdateType::Mint,
4499 12345,
4500 "0xabc123".to_string(),
4501 0,
4502 0,
4503 Some(
4504 "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
4505 .parse()
4506 .unwrap(),
4507 ),
4508 "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
4509 .parse()
4510 .unwrap(),
4511 1000,
4512 U256::from(1_000u64),
4513 U256::from(2_000u64),
4514 -10,
4515 10,
4516 UnixNanos::default(),
4517 UnixNanos::default(),
4518 )
4519 }
4520
4521 #[cfg(feature = "defi")]
4522 fn sample_pool_fee_collect() -> PoolFeeCollect {
4523 let (chain, dex, pool) = sample_pool_components();
4524 PoolFeeCollect::new(
4525 chain,
4526 dex,
4527 pool.instrument_id,
4528 pool.pool_identifier,
4529 12345,
4530 "0xabc123".to_string(),
4531 0,
4532 0,
4533 "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
4534 .parse()
4535 .unwrap(),
4536 100,
4537 200,
4538 -10,
4539 10,
4540 UnixNanos::default(),
4541 UnixNanos::default(),
4542 )
4543 }
4544
4545 #[cfg(feature = "defi")]
4546 fn sample_pool_flash() -> PoolFlash {
4547 let (chain, dex, pool) = sample_pool_components();
4548 PoolFlash::new(
4549 chain,
4550 dex,
4551 pool.instrument_id,
4552 pool.pool_identifier,
4553 12345,
4554 "0xabc123".to_string(),
4555 0,
4556 0,
4557 UnixNanos::default(),
4558 UnixNanos::default(),
4559 "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
4560 .parse()
4561 .unwrap(),
4562 "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
4563 .parse()
4564 .unwrap(),
4565 U256::from(100u64),
4566 U256::from(200u64),
4567 U256::from(101u64),
4568 U256::from(201u64),
4569 )
4570 }
4571
4572 const TRACKING_ACTOR_CODE: &std::ffi::CStr = c_str!(
4573 r#"
4574class TrackingActor:
4575 """A mock Python actor that tracks all method calls."""
4576
4577 TRACKED_METHODS = {
4578 "on_start",
4579 "on_stop",
4580 "on_resume",
4581 "on_reset",
4582 "on_dispose",
4583 "on_degrade",
4584 "on_fault",
4585 "on_save",
4586 "on_load",
4587 "on_time_event",
4588 "on_data",
4589 "on_signal",
4590 "on_queue_state",
4591 "on_socket_state",
4592 "on_instrument",
4593 "on_quote",
4594 "on_trade",
4595 "on_bar",
4596 "on_book",
4597 "on_book_deltas",
4598 "on_book_depth",
4599 "on_mark_price",
4600 "on_index_price",
4601 "on_funding_rate",
4602 "on_instrument_status",
4603 "on_instrument_close",
4604 "on_option_greeks",
4605 "on_option_chain",
4606 "on_historical_data",
4607 "on_historical_book_deltas",
4608 "on_historical_book_depth",
4609 "on_historical_quotes",
4610 "on_historical_trades",
4611 "on_historical_funding_rates",
4612 "on_historical_bars",
4613 "on_historical_mark_prices",
4614 "on_historical_index_prices",
4615 "on_block",
4616 "on_pool",
4617 "on_pool_swap",
4618 "on_pool_liquidity_update",
4619 "on_pool_fee_collect",
4620 "on_pool_flash",
4621 }
4622
4623 def __init__(self):
4624 self.calls = []
4625 self.raises = False
4626
4627 def _record(self, method_name, *args):
4628 self.calls.append((method_name, args))
4629 if self.raises:
4630 raise RuntimeError("actor callback failure")
4631
4632 def was_called(self, method_name):
4633 return any(call[0] == method_name for call in self.calls)
4634
4635 def call_count(self, method_name):
4636 return sum(1 for call in self.calls if call[0] == method_name)
4637
4638 def last_call_args(self, method_name):
4639 for called_method, args in reversed(self.calls):
4640 if called_method == method_name:
4641 return args
4642 raise AssertionError(f"{method_name} was not called")
4643
4644 def last_loaded_state(self):
4645
4646 for method_name, args in reversed(self.calls):
4647 if method_name == "on_load":
4648 return args[0]
4649 return None
4650
4651 def on_save(self):
4652 self._record("on_save")
4653 return {"actor": b"saved"}
4654
4655 def on_load(self, state):
4656 self._record("on_load", dict(state))
4657
4658 def __getattr__(self, name):
4659 if name in self.TRACKED_METHODS:
4660 return lambda *args: self._record(name, *args)
4661 raise AttributeError(name)
4662"#
4663 );
4664
4665 fn create_tracking_python_actor(py: Python<'_>) -> PyResult<Py<PyAny>> {
4666 py.run(TRACKING_ACTOR_CODE, None, None)?;
4667 let tracking_actor_class = py.eval(c_str!("TrackingActor"), None, None)?;
4668 let instance = tracking_actor_class.call0()?;
4669 Ok(instance.unbind())
4670 }
4671
4672 fn python_method_was_called(py_actor: &Py<PyAny>, py: Python<'_>, method_name: &str) -> bool {
4673 py_actor
4674 .call_method1(py, "was_called", (method_name,))
4675 .and_then(|r| r.extract::<bool>(py))
4676 .unwrap_or(false)
4677 }
4678
4679 fn python_method_call_count(py_actor: &Py<PyAny>, py: Python<'_>, method_name: &str) -> i32 {
4680 py_actor
4681 .call_method1(py, "call_count", (method_name,))
4682 .and_then(|r| r.extract::<i32>(py))
4683 .unwrap_or(0)
4684 }
4685
4686 fn python_last_loaded_state(
4687 py_actor: &Py<PyAny>,
4688 py: Python<'_>,
4689 ) -> Option<HashMap<String, Vec<u8>>> {
4690 py_actor
4691 .call_method0(py, "last_loaded_state")
4692 .and_then(|result| result.extract::<Option<HashMap<String, Vec<u8>>>>(py))
4693 .unwrap_or(None)
4694 }
4695
4696 const TRACKING_INDICATOR_CODE: &std::ffi::CStr = c_str!(
4697 r#"
4698class TrackingIndicator:
4699 def __init__(self, events=None):
4700 self.initialized = False
4701 self.calls = []
4702 self.events = events
4703
4704 def handle_quote_tick(self, quote):
4705 self.calls.append("quote")
4706 if self.events is not None:
4707 self.events.append("indicator:quote")
4708
4709 def handle_trade_tick(self, trade):
4710 self.calls.append("trade")
4711 if self.events is not None:
4712 self.events.append("indicator:trade")
4713
4714 def handle_bar(self, bar):
4715 self.calls.append("bar")
4716 if self.events is not None:
4717 self.events.append("indicator:bar")
4718
4719 def call_count(self, name):
4720 return self.calls.count(name)
4721
4722class RaisingIndicator:
4723 initialized = True
4724
4725 def handle_quote_tick(self, quote):
4726 raise RuntimeError("indicator failed")
4727
4728class IndicatorEventActor:
4729 def __init__(self, events):
4730 self.events = events
4731
4732 def on_start(self):
4733 pass
4734
4735 def on_quote(self, quote):
4736 self.events.append("actor:quote")
4737
4738 def on_trade(self, trade):
4739 self.events.append("actor:trade")
4740
4741 def on_bar(self, bar):
4742 self.events.append("actor:bar")
4743"#
4744 );
4745
4746 fn create_tracking_python_indicator(py: Python<'_>) -> PyResult<Py<PyAny>> {
4747 py.run(TRACKING_INDICATOR_CODE, None, None)?;
4748 let indicator_class = py.eval(c_str!("TrackingIndicator"), None, None)?;
4749 Ok(indicator_class.call0()?.unbind())
4750 }
4751
4752 fn create_event_tracking_python_indicator(
4753 py: Python<'_>,
4754 events: &Bound<'_, PyList>,
4755 ) -> PyResult<Py<PyAny>> {
4756 py.run(TRACKING_INDICATOR_CODE, None, None)?;
4757 let indicator_class = py.eval(c_str!("TrackingIndicator"), None, None)?;
4758 Ok(indicator_class.call1((events,))?.unbind())
4759 }
4760
4761 fn create_raising_python_indicator(py: Python<'_>) -> PyResult<Py<PyAny>> {
4762 py.run(TRACKING_INDICATOR_CODE, None, None)?;
4763 let indicator_class = py.eval(c_str!("RaisingIndicator"), None, None)?;
4764 Ok(indicator_class.call0()?.unbind())
4765 }
4766
4767 fn create_indicator_event_actor(
4768 py: Python<'_>,
4769 events: &Bound<'_, PyList>,
4770 ) -> PyResult<Py<PyAny>> {
4771 py.run(TRACKING_INDICATOR_CODE, None, None)?;
4772 let actor_class = py.eval(c_str!("IndicatorEventActor"), None, None)?;
4773 Ok(actor_class.call1((events,))?.unbind())
4774 }
4775
4776 fn python_indicator_call_count(
4777 indicator: &Py<PyAny>,
4778 py: Python<'_>,
4779 method_name: &str,
4780 ) -> i32 {
4781 indicator
4782 .call_method1(py, "call_count", (method_name,))
4783 .and_then(|r| r.extract::<i32>(py))
4784 .unwrap_or(0)
4785 }
4786
4787 fn assert_python_dispatch<F>(
4788 py: Python<'_>,
4789 clock: Rc<RefCell<VirtualClock>>,
4790 cache: Rc<RefCell<Cache>>,
4791 trader_id: TraderId,
4792 method_name: &str,
4793 invoke: F,
4794 ) -> Py<PyAny>
4795 where
4796 F: FnOnce(&mut PyDataActor) -> anyhow::Result<()>,
4797 {
4798 let py_actor = create_tracking_python_actor(py).unwrap();
4799
4800 let mut rust_actor = PyDataActor::new(None);
4801 rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
4802 rust_actor.register(trader_id, clock, cache).unwrap();
4803
4804 let result = invoke(&mut rust_actor);
4805
4806 assert!(result.is_ok());
4807 assert!(python_method_was_called(&py_actor, py, method_name));
4808 assert_eq!(python_method_call_count(&py_actor, py, method_name), 1);
4809
4810 py_actor
4811 }
4812
4813 #[rstest]
4814 fn test_python_actor_callback_exception_preserves_traceback() {
4815 Python::initialize();
4816 Python::attach(|py| {
4817 let tracker = create_tracking_python_actor(py).unwrap();
4818 tracker.setattr(py, "raises", true).unwrap();
4819 let mut actor = PyDataActor::new(None);
4820 actor.set_python_instance(tracker.bind(py)).unwrap();
4821 let error = DataActor::on_bar(actor.inner_mut(), &sample_bar()).unwrap_err();
4822 let message = error.to_string();
4823
4824 assert_eq!(python_method_call_count(&tracker, py, "on_bar"), 1);
4825 assert!(message.contains("Python on_bar failed:"));
4826 assert!(message.contains("in _record"));
4827 assert!(message.contains("RuntimeError: actor callback failure"));
4828 });
4829 }
4830
4831 #[rstest]
4832 #[case("on_start")]
4833 #[case("on_stop")]
4834 #[case("on_resume")]
4835 #[case("on_reset")]
4836 #[case("on_dispose")]
4837 #[case("on_degrade")]
4838 #[case("on_fault")]
4839 fn test_python_dispatch_lifecycle_matrix(
4840 clock: Rc<RefCell<VirtualClock>>,
4841 cache: Rc<RefCell<Cache>>,
4842 trader_id: TraderId,
4843 #[case] method_name: &str,
4844 ) {
4845 pyo3::Python::initialize();
4846
4847 Python::attach(|py| {
4848 assert_python_dispatch(py, clock, cache, trader_id, method_name, |rust_actor| {
4849 match method_name {
4850 "on_start" => DataActor::on_start(rust_actor.inner_mut()),
4851 "on_stop" => DataActor::on_stop(rust_actor.inner_mut()),
4852 "on_resume" => DataActor::on_resume(rust_actor.inner_mut()),
4853 "on_reset" => DataActor::on_reset(rust_actor.inner_mut()),
4854 "on_dispose" => DataActor::on_dispose(rust_actor.inner_mut()),
4855 "on_degrade" => DataActor::on_degrade(rust_actor.inner_mut()),
4856 "on_fault" => DataActor::on_fault(rust_actor.inner_mut()),
4857 _ => unreachable!("unhandled lifecycle case: {method_name}"),
4858 }
4859 });
4860 });
4861 }
4862
4863 #[rstest]
4864 #[case("on_save")]
4865 #[case("on_load")]
4866 fn test_python_dispatch_persistence_matrix(
4867 clock: Rc<RefCell<VirtualClock>>,
4868 cache: Rc<RefCell<Cache>>,
4869 trader_id: TraderId,
4870 #[case] method_name: &str,
4871 ) {
4872 pyo3::Python::initialize();
4873
4874 Python::attach(|py| {
4875 assert_python_dispatch(py, clock, cache, trader_id, method_name, |rust_actor| {
4876 match method_name {
4877 "on_save" => {
4878 let state = DataActor::on_save(rust_actor.inner()).unwrap();
4879 assert_eq!(
4880 state.get("actor").map(Vec::as_slice),
4881 Some(b"saved".as_slice())
4882 );
4883 Ok(())
4884 }
4885 "on_load" => {
4886 let mut state = IndexMap::new();
4887 state.insert("actor".to_string(), b"loaded".to_vec());
4888 DataActor::on_load(rust_actor.inner_mut(), state)
4889 }
4890 _ => unreachable!("unhandled persistence case: {method_name}"),
4891 }
4892 });
4893 });
4894 }
4895
4896 #[rstest]
4897 fn test_python_persistence_methods_convert_state(
4898 clock: Rc<RefCell<VirtualClock>>,
4899 cache: Rc<RefCell<Cache>>,
4900 trader_id: TraderId,
4901 ) {
4902 pyo3::Python::initialize();
4903
4904 Python::attach(|py| {
4905 let py_actor = create_tracking_python_actor(py).unwrap();
4906
4907 let mut rust_actor = PyDataActor::new(None);
4908 rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
4909 rust_actor.register(trader_id, clock, cache).unwrap();
4910
4911 let saved = rust_actor.py_save(py).unwrap();
4912 let saved_state = saved
4913 .bind(py)
4914 .extract::<HashMap<String, Vec<u8>>>()
4915 .unwrap();
4916 assert_eq!(
4917 saved_state.get("actor").map(Vec::as_slice),
4918 Some(&b"saved"[..])
4919 );
4920
4921 let load_state = PyDict::new(py);
4922 load_state
4923 .set_item("actor", PyBytes::new(py, b"loaded-from-python"))
4924 .unwrap();
4925
4926 rust_actor.py_load(&load_state).unwrap();
4927
4928 let loaded_state = python_last_loaded_state(&py_actor, py).unwrap();
4929 assert_eq!(
4930 loaded_state.get("actor").map(Vec::as_slice),
4931 Some(&b"loaded-from-python"[..])
4932 );
4933 });
4934 }
4935
4936 #[rstest]
4937 fn test_indicator_registration_exposes_readiness_and_registered_view(
4938 audusd_sim: CurrencyPair,
4939 bar_type: BarType,
4940 ) {
4941 pyo3::Python::initialize();
4942
4943 Python::attach(|py| {
4944 let mut rust_actor = PyDataActor::new(None);
4945 let indicator = create_tracking_python_indicator(py).unwrap();
4946
4947 assert_eq!(
4948 rust_actor
4949 .py_registered_indicators(py)
4950 .unwrap()
4951 .bind(py)
4952 .len()
4953 .unwrap(),
4954 0
4955 );
4956 assert!(!rust_actor.py_indicators_initialized(py).unwrap());
4957
4958 rust_actor.py_register_indicator_for_quote_ticks(
4959 py,
4960 audusd_sim.id,
4961 indicator.clone_ref(py),
4962 );
4963 rust_actor.py_register_indicator_for_trade_ticks(
4964 py,
4965 audusd_sim.id,
4966 indicator.clone_ref(py),
4967 );
4968 rust_actor.py_register_indicator_for_bars(py, bar_type, indicator.clone_ref(py));
4969
4970 let registered = rust_actor.py_registered_indicators(py).unwrap();
4971 let registered = registered.bind(py);
4972
4973 assert_eq!(registered.len().unwrap(), 1);
4974 assert_eq!(
4975 registered.get_item(0).unwrap().as_ptr(),
4976 indicator.bind(py).as_ptr()
4977 );
4978 assert!(!rust_actor.py_indicators_initialized(py).unwrap());
4979
4980 indicator.bind(py).setattr("initialized", true).unwrap();
4981
4982 assert!(rust_actor.py_indicators_initialized(py).unwrap());
4983 });
4984 }
4985
4986 #[rstest]
4987 fn test_registered_indicators_receive_quote_trade_and_bar_before_actor_callbacks(
4988 clock: Rc<RefCell<VirtualClock>>,
4989 cache: Rc<RefCell<Cache>>,
4990 trader_id: TraderId,
4991 ) {
4992 pyo3::Python::initialize();
4993
4994 Python::attach(|py| {
4995 let events = PyList::empty(py);
4996 let py_actor = create_indicator_event_actor(py, &events).unwrap();
4997 let indicator = create_event_tracking_python_indicator(py, &events).unwrap();
4998
4999 let mut rust_actor = PyDataActor::new(None);
5000 rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
5001 rust_actor.register(trader_id, clock, cache).unwrap();
5002 Component::start(rust_actor.inner_mut()).unwrap();
5003
5004 let quote = sample_quote();
5005 let trade = sample_trade();
5006 let bar = sample_bar();
5007 let external_bar_type = BarType::from_str(&format!(
5008 "{}-1-MINUTE-LAST-EXTERNAL",
5009 bar.bar_type.instrument_id()
5010 ))
5011 .unwrap();
5012
5013 rust_actor.py_register_indicator_for_quote_ticks(
5014 py,
5015 quote.instrument_id,
5016 indicator.clone_ref(py),
5017 );
5018 rust_actor.py_register_indicator_for_trade_ticks(
5019 py,
5020 trade.instrument_id,
5021 indicator.clone_ref(py),
5022 );
5023 rust_actor.py_register_indicator_for_bars(
5024 py,
5025 external_bar_type,
5026 indicator.clone_ref(py),
5027 );
5028
5029 DataActor::handle_quote(rust_actor.inner_mut(), "e);
5030 DataActor::handle_trade(rust_actor.inner_mut(), &trade);
5031 DataActor::handle_bar(rust_actor.inner_mut(), &bar);
5032
5033 let events = events.extract::<Vec<String>>().unwrap();
5034
5035 assert_eq!(python_indicator_call_count(&indicator, py, "quote"), 1);
5036 assert_eq!(python_indicator_call_count(&indicator, py, "trade"), 1);
5037 assert_eq!(python_indicator_call_count(&indicator, py, "bar"), 1);
5038 assert_eq!(
5039 events,
5040 vec![
5041 "indicator:quote",
5042 "actor:quote",
5043 "indicator:trade",
5044 "actor:trade",
5045 "indicator:bar",
5046 "actor:bar",
5047 ]
5048 );
5049 });
5050 }
5051
5052 #[rstest]
5053 fn test_registered_indicators_receive_live_data_when_actor_not_running(
5054 clock: Rc<RefCell<VirtualClock>>,
5055 cache: Rc<RefCell<Cache>>,
5056 trader_id: TraderId,
5057 ) {
5058 pyo3::Python::initialize();
5059
5060 Python::attach(|py| {
5061 let events = PyList::empty(py);
5062 let py_actor = create_indicator_event_actor(py, &events).unwrap();
5063 let indicator = create_event_tracking_python_indicator(py, &events).unwrap();
5064
5065 let mut rust_actor = PyDataActor::new(None);
5066 rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
5067 rust_actor.register(trader_id, clock, cache).unwrap();
5068
5069 let quote = sample_quote();
5070
5071 rust_actor.py_register_indicator_for_quote_ticks(
5072 py,
5073 quote.instrument_id,
5074 indicator.clone_ref(py),
5075 );
5076
5077 DataActor::handle_quote(rust_actor.inner_mut(), "e);
5078
5079 let events = events.extract::<Vec<String>>().unwrap();
5080
5081 assert_eq!(python_indicator_call_count(&indicator, py, "quote"), 1);
5082 assert_eq!(events, vec!["indicator:quote"]);
5083 });
5084 }
5085
5086 #[rstest]
5087 fn test_registered_indicators_receive_historical_quote_trade_and_bar_batches() {
5088 pyo3::Python::initialize();
5089
5090 Python::attach(|py| {
5091 let mut rust_actor = PyDataActor::new(None);
5092 let indicator = create_tracking_python_indicator(py).unwrap();
5093 let quote = sample_quote();
5094 let trade = sample_trade();
5095 let bar = sample_bar();
5096 let quotes = vec![quote];
5097 let trades = vec![trade];
5098 let bars = vec![bar];
5099
5100 rust_actor.py_register_indicator_for_quote_ticks(
5101 py,
5102 quote.instrument_id,
5103 indicator.clone_ref(py),
5104 );
5105 rust_actor.py_register_indicator_for_trade_ticks(
5106 py,
5107 trade.instrument_id,
5108 indicator.clone_ref(py),
5109 );
5110 rust_actor.py_register_indicator_for_bars(py, bar.bar_type, indicator.clone_ref(py));
5111
5112 let client_id = ClientId::new("TEST");
5113 let quotes_response = QuotesResponse::new(
5114 UUID4::new(),
5115 client_id,
5116 quote.instrument_id,
5117 quotes,
5118 None,
5119 None,
5120 UnixNanos::default(),
5121 None,
5122 );
5123 let trades_response = TradesResponse::new(
5124 UUID4::new(),
5125 client_id,
5126 trade.instrument_id,
5127 trades,
5128 None,
5129 None,
5130 UnixNanos::default(),
5131 None,
5132 );
5133 let bars_response = BarsResponse::new(
5134 UUID4::new(),
5135 client_id,
5136 bar.bar_type,
5137 bars,
5138 None,
5139 None,
5140 UnixNanos::default(),
5141 None,
5142 );
5143
5144 DataActor::handle_quotes_response(rust_actor.inner_mut(), "es_response);
5145 DataActor::handle_trades_response(rust_actor.inner_mut(), &trades_response);
5146 DataActor::handle_bars_response(rust_actor.inner_mut(), &bars_response);
5147
5148 assert_eq!(python_indicator_call_count(&indicator, py, "quote"), 1);
5149 assert_eq!(python_indicator_call_count(&indicator, py, "trade"), 1);
5150 assert_eq!(python_indicator_call_count(&indicator, py, "bar"), 1);
5151 });
5152 }
5153
5154 #[rstest]
5155 fn test_indicators_initialized_requires_all_registered_indicators(audusd_sim: CurrencyPair) {
5156 pyo3::Python::initialize();
5157
5158 Python::attach(|py| {
5159 let mut rust_actor = PyDataActor::new(None);
5160 let first = create_tracking_python_indicator(py).unwrap();
5161 let second = create_tracking_python_indicator(py).unwrap();
5162
5163 rust_actor.py_register_indicator_for_quote_ticks(
5164 py,
5165 audusd_sim.id,
5166 first.clone_ref(py),
5167 );
5168 rust_actor.py_register_indicator_for_quote_ticks(
5169 py,
5170 audusd_sim.id,
5171 second.clone_ref(py),
5172 );
5173
5174 first.bind(py).setattr("initialized", true).unwrap();
5175
5176 assert!(!rust_actor.py_indicators_initialized(py).unwrap());
5177
5178 second.bind(py).setattr("initialized", true).unwrap();
5179
5180 assert!(rust_actor.py_indicators_initialized(py).unwrap());
5181 });
5182 }
5183
5184 #[rstest]
5185 fn test_duplicate_indicator_registration_does_not_duplicate_callbacks(
5186 clock: Rc<RefCell<VirtualClock>>,
5187 cache: Rc<RefCell<Cache>>,
5188 trader_id: TraderId,
5189 ) {
5190 pyo3::Python::initialize();
5191
5192 Python::attach(|py| {
5193 let mut rust_actor = PyDataActor::new(None);
5194 let indicator = create_tracking_python_indicator(py).unwrap();
5195 let quote = sample_quote();
5196 let trade = sample_trade();
5197 let bar = sample_bar();
5198 rust_actor.register(trader_id, clock, cache).unwrap();
5199 Component::start(rust_actor.inner_mut()).unwrap();
5200
5201 rust_actor.py_register_indicator_for_quote_ticks(
5202 py,
5203 quote.instrument_id,
5204 indicator.clone_ref(py),
5205 );
5206 rust_actor.py_register_indicator_for_quote_ticks(
5207 py,
5208 quote.instrument_id,
5209 indicator.clone_ref(py),
5210 );
5211 rust_actor.py_register_indicator_for_trade_ticks(
5212 py,
5213 trade.instrument_id,
5214 indicator.clone_ref(py),
5215 );
5216 rust_actor.py_register_indicator_for_trade_ticks(
5217 py,
5218 trade.instrument_id,
5219 indicator.clone_ref(py),
5220 );
5221 rust_actor.py_register_indicator_for_bars(py, bar.bar_type, indicator.clone_ref(py));
5222 rust_actor.py_register_indicator_for_bars(py, bar.bar_type, indicator.clone_ref(py));
5223
5224 DataActor::handle_quote(rust_actor.inner_mut(), "e);
5225 DataActor::handle_trade(rust_actor.inner_mut(), &trade);
5226 DataActor::handle_bar(rust_actor.inner_mut(), &bar);
5227
5228 assert_eq!(
5229 rust_actor
5230 .py_registered_indicators(py)
5231 .unwrap()
5232 .bind(py)
5233 .len()
5234 .unwrap(),
5235 1
5236 );
5237 assert_eq!(python_indicator_call_count(&indicator, py, "quote"), 1);
5238 assert_eq!(python_indicator_call_count(&indicator, py, "trade"), 1);
5239 assert_eq!(python_indicator_call_count(&indicator, py, "bar"), 1);
5240 });
5241 }
5242
5243 #[rstest]
5244 fn test_indicator_error_prevents_actor_callback(
5245 clock: Rc<RefCell<VirtualClock>>,
5246 cache: Rc<RefCell<Cache>>,
5247 trader_id: TraderId,
5248 ) {
5249 pyo3::Python::initialize();
5250
5251 Python::attach(|py| {
5252 let events = PyList::empty(py);
5253 let py_actor = create_indicator_event_actor(py, &events).unwrap();
5254 let indicator = create_raising_python_indicator(py).unwrap();
5255
5256 let mut rust_actor = PyDataActor::new(None);
5257 rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
5258 rust_actor.register(trader_id, clock, cache).unwrap();
5259 Component::start(rust_actor.inner_mut()).unwrap();
5260
5261 let quote = sample_quote();
5262
5263 rust_actor.py_register_indicator_for_quote_ticks(py, quote.instrument_id, indicator);
5264
5265 DataActor::handle_quote(rust_actor.inner_mut(), "e);
5266 let events = events.extract::<Vec<String>>().unwrap();
5267
5268 assert!(events.is_empty());
5269 });
5270 }
5271
5272 #[rstest]
5273 #[case("on_time_event")]
5274 #[case("on_data")]
5275 #[case("on_signal")]
5276 #[case("on_queue_state")]
5277 #[case("on_socket_state")]
5278 #[case("on_instrument")]
5279 #[case("on_quote")]
5280 #[case("on_trade")]
5281 #[case("on_bar")]
5282 #[case("on_book")]
5283 #[case("on_book_deltas")]
5284 #[case("on_book_depth")]
5285 #[case("on_mark_price")]
5286 #[case("on_index_price")]
5287 #[case("on_funding_rate")]
5288 #[case("on_instrument_status")]
5289 #[case("on_instrument_close")]
5290 #[case("on_option_greeks")]
5291 #[case("on_option_chain")]
5292 fn test_python_dispatch_typed_callback_matrix(
5293 clock: Rc<RefCell<VirtualClock>>,
5294 cache: Rc<RefCell<Cache>>,
5295 trader_id: TraderId,
5296 #[case] method_name: &str,
5297 ) {
5298 pyo3::Python::initialize();
5299
5300 Python::attach(|py| {
5301 assert_python_dispatch(py, clock, cache, trader_id, method_name, |rust_actor| {
5302 match method_name {
5303 "on_time_event" => {
5304 let event = sample_time_event();
5305 rust_actor.inner_mut().on_time_event(&event)
5306 }
5307 "on_data" => {
5308 let data = sample_data();
5309 rust_actor.inner_mut().on_data(&data)
5310 }
5311 "on_signal" => {
5312 let signal = sample_signal();
5313 rust_actor.inner_mut().on_signal(&signal)
5314 }
5315 "on_queue_state" => {
5316 let event = sample_queue_state_changed(QueueState::Triggered);
5317 rust_actor.inner_mut().on_queue_state(&event)
5318 }
5319 "on_socket_state" => {
5320 let event = sample_socket_state_changed(SocketState::Connected);
5321 rust_actor.inner_mut().on_socket_state(&event)
5322 }
5323 "on_instrument" => {
5324 let instrument = InstrumentAny::CurrencyPair(sample_instrument());
5325 rust_actor.inner_mut().on_instrument(&instrument)
5326 }
5327 "on_quote" => {
5328 let quote = sample_quote();
5329 rust_actor.inner_mut().on_quote("e)
5330 }
5331 "on_trade" => {
5332 let trade = sample_trade();
5333 rust_actor.inner_mut().on_trade(&trade)
5334 }
5335 "on_bar" => {
5336 let bar = sample_bar();
5337 rust_actor.inner_mut().on_bar(&bar)
5338 }
5339 "on_book" => {
5340 let book = sample_book();
5341 rust_actor.inner_mut().on_book(&book)
5342 }
5343 "on_book_deltas" => {
5344 let deltas = sample_book_deltas();
5345 rust_actor.inner_mut().on_book_deltas(&deltas)
5346 }
5347 "on_book_depth" => {
5348 let depth = sample_book_depth();
5349 rust_actor.inner_mut().on_book_depth(&depth)
5350 }
5351 "on_mark_price" => {
5352 let update = sample_mark_price();
5353 rust_actor.inner_mut().on_mark_price(&update)
5354 }
5355 "on_index_price" => {
5356 let update = sample_index_price();
5357 rust_actor.inner_mut().on_index_price(&update)
5358 }
5359 "on_funding_rate" => {
5360 let update = sample_funding_rate();
5361 rust_actor.inner_mut().on_funding_rate(&update)
5362 }
5363 "on_instrument_status" => {
5364 let status = sample_instrument_status();
5365 rust_actor.inner_mut().on_instrument_status(&status)
5366 }
5367 "on_instrument_close" => {
5368 let close = sample_instrument_close();
5369 rust_actor.inner_mut().on_instrument_close(&close)
5370 }
5371 "on_option_greeks" => {
5372 let greeks = sample_option_greeks();
5373 rust_actor.inner_mut().on_option_greeks(&greeks)
5374 }
5375 "on_option_chain" => {
5376 let chain = sample_option_chain();
5377 rust_actor.inner_mut().on_option_chain(&chain)
5378 }
5379 _ => unreachable!("unhandled typed callback case: {method_name}"),
5380 }
5381 });
5382 });
5383 }
5384
5385 #[rstest]
5386 #[case("on_historical_data")]
5387 #[case("on_historical_book_deltas")]
5388 #[case("on_historical_book_depth")]
5389 #[case("on_historical_quotes")]
5390 #[case("on_historical_trades")]
5391 #[case("on_historical_funding_rates")]
5392 #[case("on_historical_bars")]
5393 #[case("on_historical_mark_prices")]
5394 #[case("on_historical_index_prices")]
5395 fn test_python_dispatch_historical_callback_matrix(
5396 clock: Rc<RefCell<VirtualClock>>,
5397 cache: Rc<RefCell<Cache>>,
5398 trader_id: TraderId,
5399 #[case] method_name: &str,
5400 ) {
5401 pyo3::Python::initialize();
5402
5403 Python::attach(|py| {
5404 assert_python_dispatch(py, clock, cache, trader_id, method_name, |rust_actor| {
5405 match method_name {
5406 "on_historical_data" => {
5407 let data = sample_data();
5408 rust_actor.inner_mut().on_historical_data(&data)
5409 }
5410 "on_historical_book_deltas" => {
5411 let deltas = sample_book_deltas().deltas;
5412 rust_actor.inner_mut().on_historical_book_deltas(&deltas)
5413 }
5414 "on_historical_book_depth" => {
5415 let depths = vec![sample_book_depth()];
5416 rust_actor.inner_mut().on_historical_book_depth(&depths)
5417 }
5418 "on_historical_quotes" => {
5419 let quotes = vec![sample_quote()];
5420 rust_actor.inner_mut().on_historical_quotes("es)
5421 }
5422 "on_historical_trades" => {
5423 let trades = vec![sample_trade()];
5424 rust_actor.inner_mut().on_historical_trades(&trades)
5425 }
5426 "on_historical_funding_rates" => {
5427 let funding_rates = vec![sample_funding_rate()];
5428 rust_actor
5429 .inner_mut()
5430 .on_historical_funding_rates(&funding_rates)
5431 }
5432 "on_historical_bars" => {
5433 let bars = vec![sample_bar()];
5434 rust_actor.inner_mut().on_historical_bars(&bars)
5435 }
5436 "on_historical_mark_prices" => {
5437 let mark_prices = vec![sample_mark_price()];
5438 rust_actor
5439 .inner_mut()
5440 .on_historical_mark_prices(&mark_prices)
5441 }
5442 "on_historical_index_prices" => {
5443 let index_prices = vec![sample_index_price()];
5444 rust_actor
5445 .inner_mut()
5446 .on_historical_index_prices(&index_prices)
5447 }
5448 _ => unreachable!("unhandled historical callback case: {method_name}"),
5449 }
5450 });
5451 });
5452 }
5453
5454 #[rstest]
5455 fn test_python_dispatch_historical_book_deltas_preserves_batch(
5456 clock: Rc<RefCell<VirtualClock>>,
5457 cache: Rc<RefCell<Cache>>,
5458 trader_id: TraderId,
5459 ) {
5460 pyo3::Python::initialize();
5461
5462 Python::attach(|py| {
5463 let expected = stub_deltas().deltas;
5464 let py_actor = assert_python_dispatch(
5465 py,
5466 clock,
5467 cache,
5468 trader_id,
5469 "on_historical_book_deltas",
5470 |rust_actor| rust_actor.inner_mut().on_historical_book_deltas(&expected),
5471 );
5472 let actual = py_actor
5473 .call_method1(py, "last_call_args", ("on_historical_book_deltas",))
5474 .unwrap()
5475 .bind(py)
5476 .get_item(0)
5477 .unwrap()
5478 .extract::<Vec<OrderBookDelta>>()
5479 .unwrap();
5480
5481 assert_eq!(actual, expected);
5482 });
5483 }
5484
5485 #[rstest]
5486 fn test_python_dispatch_historical_book_depth_preserves_batch(
5487 clock: Rc<RefCell<VirtualClock>>,
5488 cache: Rc<RefCell<Cache>>,
5489 trader_id: TraderId,
5490 ) {
5491 pyo3::Python::initialize();
5492
5493 Python::attach(|py| {
5494 let first = stub_depth10();
5495 let mut second = first.clone();
5496 second.sequence = 17;
5497 second.ts_event = UnixNanos::from(18);
5498 second.ts_init = UnixNanos::from(19);
5499 let expected = vec![first, second];
5500 let py_actor = assert_python_dispatch(
5501 py,
5502 clock,
5503 cache,
5504 trader_id,
5505 "on_historical_book_depth",
5506 |rust_actor| rust_actor.inner_mut().on_historical_book_depth(&expected),
5507 );
5508 let actual = py_actor
5509 .call_method1(py, "last_call_args", ("on_historical_book_depth",))
5510 .unwrap()
5511 .bind(py)
5512 .get_item(0)
5513 .unwrap()
5514 .extract::<Vec<OrderBookDepth>>()
5515 .unwrap();
5516
5517 assert_eq!(actual, expected);
5518 });
5519 }
5520
5521 #[cfg(feature = "defi")]
5522 #[rstest]
5523 #[case("on_block")]
5524 #[case("on_pool")]
5525 #[case("on_pool_swap")]
5526 #[case("on_pool_liquidity_update")]
5527 #[case("on_pool_fee_collect")]
5528 #[case("on_pool_flash")]
5529 fn test_python_dispatch_defi_callback_matrix(
5530 clock: Rc<RefCell<VirtualClock>>,
5531 cache: Rc<RefCell<Cache>>,
5532 trader_id: TraderId,
5533 #[case] method_name: &str,
5534 ) {
5535 pyo3::Python::initialize();
5536
5537 Python::attach(|py| {
5538 assert_python_dispatch(py, clock, cache, trader_id, method_name, |rust_actor| {
5539 match method_name {
5540 "on_block" => {
5541 let block = sample_block();
5542 rust_actor.inner_mut().on_block(&block)
5543 }
5544 "on_pool" => {
5545 let (_chain, _dex, pool) = sample_pool_components();
5546 rust_actor.inner_mut().on_pool(&pool)
5547 }
5548 "on_pool_swap" => {
5549 let swap = sample_pool_swap();
5550 rust_actor.inner_mut().on_pool_swap(&swap)
5551 }
5552 "on_pool_liquidity_update" => {
5553 let update = sample_pool_liquidity_update();
5554 rust_actor.inner_mut().on_pool_liquidity_update(&update)
5555 }
5556 "on_pool_fee_collect" => {
5557 let collect = sample_pool_fee_collect();
5558 rust_actor.inner_mut().on_pool_fee_collect(&collect)
5559 }
5560 "on_pool_flash" => {
5561 let flash = sample_pool_flash();
5562 rust_actor.inner_mut().on_pool_flash(&flash)
5563 }
5564 _ => unreachable!("unhandled defi callback case: {method_name}"),
5565 }
5566 });
5567 });
5568 }
5569
5570 #[rstest]
5571 fn test_python_dispatch_multiple_calls_tracked(
5572 clock: Rc<RefCell<VirtualClock>>,
5573 cache: Rc<RefCell<Cache>>,
5574 trader_id: TraderId,
5575 audusd_sim: CurrencyPair,
5576 ) {
5577 pyo3::Python::initialize();
5578
5579 Python::attach(|py| {
5580 let py_actor = create_tracking_python_actor(py).unwrap();
5581
5582 let mut rust_actor = PyDataActor::new(None);
5583 rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
5584 rust_actor.register(trader_id, clock, cache).unwrap();
5585
5586 let quote = QuoteTick::new(
5587 audusd_sim.id,
5588 Price::from("1.00000"),
5589 Price::from("1.00001"),
5590 Quantity::from(100_000),
5591 Quantity::from(100_000),
5592 UnixNanos::default(),
5593 UnixNanos::default(),
5594 );
5595
5596 rust_actor.inner_mut().on_quote("e).unwrap();
5597 rust_actor.inner_mut().on_quote("e).unwrap();
5598 rust_actor.inner_mut().on_quote("e).unwrap();
5599
5600 assert_eq!(python_method_call_count(&py_actor, py, "on_quote"), 3);
5601 });
5602 }
5603
5604 #[rstest]
5605 fn test_python_dispatch_historical_custom_data_preserves_payload_shape(
5606 clock: Rc<RefCell<VirtualClock>>,
5607 cache: Rc<RefCell<Cache>>,
5608 trader_id: TraderId,
5609 client_id: ClientId,
5610 ) {
5611 pyo3::Python::initialize();
5612
5613 Python::attach(|py| {
5614 let py_actor = create_tracking_python_actor(py).unwrap();
5615 let mut rust_actor = PyDataActor::new(None);
5616 rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
5617 rust_actor.register(trader_id, clock, cache).unwrap();
5618
5619 let data = vec![
5620 stub_custom_data(1, 42, None, None),
5621 stub_custom_data(2, 84, None, None),
5622 ];
5623 let scalar = stub_custom_data(3, 126, None, None);
5624 let scalar_response = CustomDataResponse::new(
5625 UUID4::new(),
5626 client_id,
5627 None,
5628 scalar.data_type.clone(),
5629 scalar.clone(),
5630 None,
5631 None,
5632 UnixNanos::default(),
5633 None,
5634 );
5635
5636 DataActor::handle_data_response(rust_actor.inner_mut(), &scalar_response);
5637
5638 let actual_scalar = py_actor
5639 .call_method1(py, "last_call_args", ("on_historical_data",))
5640 .unwrap()
5641 .bind(py)
5642 .get_item(0)
5643 .unwrap()
5644 .extract::<CustomData>()
5645 .unwrap();
5646
5647 assert_eq!(
5648 python_method_call_count(&py_actor, py, "on_historical_data"),
5649 1
5650 );
5651 assert_eq!(actual_scalar, scalar);
5652
5653 let empty_response = CustomDataResponse::new(
5654 UUID4::new(),
5655 client_id,
5656 None,
5657 data[0].data_type.clone(),
5658 Vec::<CustomData>::new(),
5659 None,
5660 None,
5661 UnixNanos::default(),
5662 None,
5663 );
5664
5665 DataActor::handle_data_response(rust_actor.inner_mut(), &empty_response);
5666
5667 let empty = py_actor
5668 .call_method1(py, "last_call_args", ("on_historical_data",))
5669 .unwrap()
5670 .bind(py)
5671 .get_item(0)
5672 .unwrap()
5673 .extract::<Vec<CustomData>>()
5674 .unwrap();
5675
5676 assert_eq!(
5677 python_method_call_count(&py_actor, py, "on_historical_data"),
5678 2
5679 );
5680 assert!(empty.is_empty());
5681
5682 let response = CustomDataResponse::new(
5683 UUID4::new(),
5684 client_id,
5685 None,
5686 data[0].data_type.clone(),
5687 data.clone(),
5688 None,
5689 None,
5690 UnixNanos::default(),
5691 None,
5692 );
5693
5694 DataActor::handle_data_response(rust_actor.inner_mut(), &response);
5695
5696 let actual = py_actor
5697 .call_method1(py, "last_call_args", ("on_historical_data",))
5698 .unwrap()
5699 .bind(py)
5700 .get_item(0)
5701 .unwrap()
5702 .extract::<Vec<CustomData>>()
5703 .unwrap();
5704
5705 assert_eq!(
5706 python_method_call_count(&py_actor, py, "on_historical_data"),
5707 3
5708 );
5709 assert_eq!(actual, data);
5710 });
5711 }
5712
5713 #[rstest]
5714 fn test_python_dispatch_no_call_when_py_self_not_set(
5715 clock: Rc<RefCell<VirtualClock>>,
5716 cache: Rc<RefCell<Cache>>,
5717 trader_id: TraderId,
5718 ) {
5719 pyo3::Python::initialize();
5720
5721 Python::attach(|_py| {
5722 let mut rust_actor = PyDataActor::new(None);
5723 rust_actor.register(trader_id, clock, cache).unwrap();
5724
5725 let result = DataActor::on_start(rust_actor.inner_mut());
5727 assert!(result.is_ok());
5728 });
5729 }
5730
5731 #[rstest]
5732 fn test_python_on_historical_data_rejects_non_custom_data(
5733 clock: Rc<RefCell<VirtualClock>>,
5734 cache: Rc<RefCell<Cache>>,
5735 trader_id: TraderId,
5736 ) {
5737 pyo3::Python::initialize();
5738
5739 let mut rust_actor = PyDataActor::new(None);
5740 rust_actor.register(trader_id, clock, cache).unwrap();
5741
5742 let non_custom: String = "not CustomData".to_string();
5743 let result = rust_actor.inner_mut().on_historical_data(&non_custom);
5744
5745 assert!(result.is_err());
5746 assert!(result.unwrap_err().to_string().contains("unsupported type"));
5747 }
5748
5749 #[rstest]
5750 fn test_python_self_is_weak() {
5751 Python::initialize();
5752
5753 Python::attach(|py| {
5754 let instance = py
5755 .get_type::<PyDataActor>()
5756 .call0()
5757 .expect("DataActor should construct");
5758 let weakref =
5759 PyWeakrefReference::new(&instance).expect("DataActor should be weak-referenceable");
5760 assert!(weakref.upgrade().is_some());
5761
5762 drop(instance);
5763
5764 assert!(
5766 weakref.upgrade().is_none(),
5767 "an unregistered DataActor must be collected once its last Python owner is dropped",
5768 );
5769 });
5770 }
5771
5772 #[rstest::rstest]
5773 #[case("on_start")]
5774 #[case("on_stop")]
5775 #[case("on_resume")]
5776 #[case("on_reset")]
5777 #[case("on_dispose")]
5778 #[case("on_degrade")]
5779 #[case("on_fault")]
5780 #[case("on_save")]
5781 #[case("on_load")]
5782 #[case("on_time_event")]
5783 #[case("on_data")]
5784 #[case("on_signal")]
5785 #[case("on_queue_state")]
5786 #[case("on_socket_state")]
5787 #[case("on_instrument")]
5788 #[case("on_quote")]
5789 #[case("on_trade")]
5790 #[case("on_bar")]
5791 #[case("on_book_deltas")]
5792 #[case("on_book_depth")]
5793 #[case("on_book")]
5794 #[case("on_mark_price")]
5795 #[case("on_index_price")]
5796 #[case("on_funding_rate")]
5797 #[case("on_instrument_status")]
5798 #[case("on_instrument_close")]
5799 #[case("on_option_greeks")]
5800 #[case("on_option_chain")]
5801 #[case("on_historical_data")]
5802 #[case("on_historical_book_deltas")]
5803 #[case("on_historical_book_depth")]
5804 #[case("on_historical_quotes")]
5805 #[case("on_historical_trades")]
5806 #[case("on_historical_funding_rates")]
5807 #[case("on_historical_bars")]
5808 #[case("on_historical_mark_prices")]
5809 #[case("on_historical_index_prices")]
5810 fn test_default_callback_during_exclusive_borrow(#[case] method: &str) {
5811 Python::initialize();
5812 Python::attach(|py| {
5813 let argument = match method {
5814 "on_start" | "on_stop" | "on_resume" | "on_reset" | "on_dispose" | "on_degrade"
5815 | "on_fault" | "on_save" => None,
5816 "on_load" => Some(PyDict::new(py).into_any().unbind()),
5817 "on_time_event" => Some(sample_time_event().into_py_any(py).unwrap()),
5818 "on_data" | "on_instrument" | "on_historical_data" => Some(py.None()),
5819 "on_signal" => Some(sample_signal().into_py_any(py).unwrap()),
5820 "on_queue_state" => Some(
5821 sample_queue_state_changed(QueueState::Triggered)
5822 .into_py_any(py)
5823 .unwrap(),
5824 ),
5825 "on_socket_state" => Some(
5826 sample_socket_state_changed(SocketState::Connected)
5827 .into_py_any(py)
5828 .unwrap(),
5829 ),
5830 "on_quote" => Some(sample_quote().into_py_any(py).unwrap()),
5831 "on_trade" => Some(sample_trade().into_py_any(py).unwrap()),
5832 "on_bar" => Some(sample_bar().into_py_any(py).unwrap()),
5833 "on_book_deltas" => Some(sample_book_deltas().into_py_any(py).unwrap()),
5834 "on_book_depth" => Some(sample_book_depth().into_py_any(py).unwrap()),
5835 "on_book" => Some(sample_book().into_py_any(py).unwrap()),
5836 "on_mark_price" => Some(sample_mark_price().into_py_any(py).unwrap()),
5837 "on_index_price" => Some(sample_index_price().into_py_any(py).unwrap()),
5838 "on_funding_rate" => Some(sample_funding_rate().into_py_any(py).unwrap()),
5839 "on_instrument_status" => Some(sample_instrument_status().into_py_any(py).unwrap()),
5840 "on_instrument_close" => Some(sample_instrument_close().into_py_any(py).unwrap()),
5841 "on_option_greeks" => Some(sample_option_greeks().into_py_any(py).unwrap()),
5842 "on_option_chain" => Some(sample_option_chain().into_py_any(py).unwrap()),
5843 "on_historical_book_deltas"
5844 | "on_historical_book_depth"
5845 | "on_historical_quotes"
5846 | "on_historical_trades"
5847 | "on_historical_funding_rates"
5848 | "on_historical_bars"
5849 | "on_historical_mark_prices"
5850 | "on_historical_index_prices" => Some(PyList::empty(py).into_any().unbind()),
5851 _ => unreachable!(),
5852 };
5853
5854 let instance = py.get_type::<PyDataActor>().call0().unwrap();
5855 let _borrow = instance
5856 .extract::<pyo3::PyRefMut<'_, PyDataActor>>()
5857 .unwrap();
5858
5859 let result = match argument {
5860 Some(argument) => instance.call_method1(method, (argument,)),
5861 None => instance.call_method0(method),
5862 }
5863 .unwrap();
5864
5865 if method == "on_save" {
5866 assert_eq!(result.len().unwrap(), 0);
5867 } else {
5868 assert!(result.is_none());
5869 }
5870 });
5871 }
5872
5873 #[cfg(feature = "defi")]
5874 #[rstest::rstest]
5875 #[case("on_block")]
5876 #[case("on_pool")]
5877 #[case("on_pool_swap")]
5878 #[case("on_pool_liquidity_update")]
5879 #[case("on_pool_fee_collect")]
5880 #[case("on_pool_flash")]
5881 fn test_default_defi_callback_during_exclusive_borrow(#[case] method: &str) {
5882 Python::initialize();
5883 Python::attach(|py| {
5884 let argument = match method {
5885 "on_block" => sample_block().into_py_any(py).unwrap(),
5886 "on_pool" => sample_pool_components().2.into_py_any(py).unwrap(),
5887 "on_pool_swap" => sample_pool_swap().into_py_any(py).unwrap(),
5888 "on_pool_liquidity_update" => {
5889 sample_pool_liquidity_update().into_py_any(py).unwrap()
5890 }
5891 "on_pool_fee_collect" => sample_pool_fee_collect().into_py_any(py).unwrap(),
5892 "on_pool_flash" => sample_pool_flash().into_py_any(py).unwrap(),
5893 _ => unreachable!(),
5894 };
5895
5896 let instance = py.get_type::<PyDataActor>().call0().unwrap();
5897 let _borrow = instance
5898 .extract::<pyo3::PyRefMut<'_, PyDataActor>>()
5899 .unwrap();
5900 assert!(
5901 instance
5902 .call_method1(method, (argument,))
5903 .unwrap()
5904 .is_none()
5905 );
5906 });
5907 }
5908}