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