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