1#![allow(dead_code)]
18#![allow(unused_variables)]
19
20use std::{
21 num::NonZeroUsize,
22 ops::{Deref, DerefMut},
23};
24
25use indexmap::IndexMap;
26use nautilus_core::{
27 nanos::UnixNanos,
28 python::{to_pyruntime_err, to_pyvalue_err},
29};
30use nautilus_model::{
31 data::{BarType, DataType},
32 enums::BookType,
33 identifiers::{ActorId, ClientId, InstrumentId, TraderId, Venue},
34};
35use pyo3::{exceptions::PyValueError, prelude::*};
36
37use crate::{
38 actor::{
39 DataActor,
40 data_actor::{DataActorConfig, DataActorCore},
41 },
42 component::Component,
43 enums::ComponentState,
44};
45
46#[allow(non_camel_case_types)]
47#[pyo3::pyclass(
48 module = "posei_trader.core.nautilus_pyo3.common",
49 name = "DataActor",
50 unsendable,
51 subclass
52)]
53#[derive(Debug)]
54pub struct PyDataActor {
55 core: DataActorCore,
56}
57
58impl Deref for PyDataActor {
59 type Target = DataActorCore;
60
61 fn deref(&self) -> &Self::Target {
62 &self.core
63 }
64}
65
66impl DerefMut for PyDataActor {
67 fn deref_mut(&mut self) -> &mut Self::Target {
68 &mut self.core
69 }
70}
71
72impl DataActor for PyDataActor {}
73
74#[pymethods]
75impl PyDataActor {
76 #[new]
77 #[pyo3(signature = (_config=None))]
78 fn py_new(_config: Option<PyObject>) -> PyResult<Self> {
79 let config = DataActorConfig::default();
81
82 Ok(Self {
83 core: DataActorCore::new(config),
84 })
85 }
86
87 #[getter]
88 #[pyo3(name = "actor_id")]
89 fn py_actor_id(&self) -> ActorId {
90 self.actor_id
91 }
92
93 #[getter]
94 #[pyo3(name = "trader_id")]
95 fn py_trader_id(&self) -> Option<TraderId> {
96 self.trader_id()
97 }
98
99 #[pyo3(name = "state")]
100 fn py_state(&self) -> ComponentState {
101 self.state()
102 }
103
104 #[pyo3(name = "is_ready")]
105 fn py_is_ready(&self) -> bool {
106 self.is_ready()
107 }
108
109 #[pyo3(name = "is_running")]
110 fn py_is_running(&self) -> bool {
111 self.is_running()
112 }
113
114 #[pyo3(name = "is_stopped")]
115 fn py_is_stopped(&self) -> bool {
116 self.is_stopped()
117 }
118
119 #[pyo3(name = "is_degraded")]
120 fn py_is_degraded(&self) -> bool {
121 self.is_degraded()
122 }
123
124 #[pyo3(name = "is_faulted")]
125 fn py_is_faulted(&self) -> bool {
126 self.is_faulted()
127 }
128
129 #[pyo3(name = "is_disposed")]
130 fn py_is_disposed(&self) -> bool {
131 self.is_disposed()
132 }
133
134 #[pyo3(name = "start")]
135 fn py_start(&mut self) -> PyResult<()> {
136 self.start().map_err(to_pyruntime_err)
137 }
138
139 #[pyo3(name = "stop")]
140 fn py_stop(&mut self) -> PyResult<()> {
141 self.stop().map_err(to_pyruntime_err)
142 }
143
144 #[pyo3(name = "resume")]
145 fn py_resume(&mut self) -> PyResult<()> {
146 self.resume().map_err(to_pyruntime_err)
147 }
148
149 #[pyo3(name = "reset")]
150 fn py_reset(&mut self) -> PyResult<()> {
151 self.reset().map_err(to_pyruntime_err)
152 }
153
154 #[pyo3(name = "dispose")]
155 fn py_dispose(&mut self) -> PyResult<()> {
156 self.dispose().map_err(to_pyruntime_err)
157 }
158
159 #[pyo3(name = "degrade")]
160 fn py_degrade(&mut self) -> PyResult<()> {
161 self.degrade().map_err(to_pyruntime_err)
162 }
163
164 #[pyo3(name = "fault")]
165 fn py_fault(&mut self) -> PyResult<()> {
166 self.fault().map_err(to_pyruntime_err)
167 }
168
169 #[pyo3(name = "shutdown_system")]
170 #[pyo3(signature = (reason=None))]
171 fn py_shutdown_system(&self, reason: Option<String>) -> PyResult<()> {
172 self.core.shutdown_system(reason);
173 Ok(())
174 }
175
176 #[pyo3(name = "subscribe_data")]
177 #[pyo3(signature = (data_type, client_id=None, params=None))]
178 fn py_subscribe_data(
179 &mut self,
180 data_type: DataType,
181 client_id: Option<ClientId>,
182 params: Option<IndexMap<String, String>>,
183 ) -> PyResult<()> {
184 self.subscribe_data(data_type, client_id, params);
185 Ok(())
186 }
187
188 #[pyo3(name = "subscribe_instruments")]
189 #[pyo3(signature = (venue, client_id=None, params=None))]
190 fn py_subscribe_instruments(
191 &mut self,
192 venue: Venue,
193 client_id: Option<ClientId>,
194 params: Option<IndexMap<String, String>>,
195 ) -> PyResult<()> {
196 self.subscribe_instruments(venue, client_id, params);
197 Ok(())
198 }
199
200 #[pyo3(name = "subscribe_instrument")]
201 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
202 fn py_subscribe_instrument(
203 &mut self,
204 instrument_id: InstrumentId,
205 client_id: Option<ClientId>,
206 params: Option<IndexMap<String, String>>,
207 ) -> PyResult<()> {
208 self.subscribe_instrument(instrument_id, client_id, params);
209 Ok(())
210 }
211
212 #[pyo3(name = "subscribe_book_deltas")]
213 #[pyo3(signature = (instrument_id, book_type, depth=None, client_id=None, managed=false, params=None))]
214 fn py_subscribe_book_deltas(
215 &mut self,
216 instrument_id: InstrumentId,
217 book_type: BookType,
218 depth: Option<usize>,
219 client_id: Option<ClientId>,
220 managed: bool,
221 params: Option<IndexMap<String, String>>,
222 ) -> PyResult<()> {
223 let depth = depth.and_then(NonZeroUsize::new);
224 self.subscribe_book_deltas(instrument_id, book_type, depth, client_id, managed, params);
225 Ok(())
226 }
227
228 #[pyo3(name = "subscribe_book_at_interval")]
229 #[pyo3(signature = (instrument_id, book_type, interval_ms, depth=None, client_id=None, params=None))]
230 fn py_subscribe_book_at_interval(
231 &mut self,
232 instrument_id: InstrumentId,
233 book_type: BookType,
234 interval_ms: usize,
235 depth: Option<usize>,
236 client_id: Option<ClientId>,
237 params: Option<IndexMap<String, String>>,
238 ) -> PyResult<()> {
239 let depth = depth.and_then(NonZeroUsize::new);
240 let interval_ms = NonZeroUsize::new(interval_ms)
241 .ok_or_else(|| PyErr::new::<PyValueError, _>("interval_ms must be > 0"))?;
242
243 self.subscribe_book_at_interval(
244 instrument_id,
245 book_type,
246 depth,
247 interval_ms,
248 client_id,
249 params,
250 );
251 Ok(())
252 }
253
254 #[pyo3(name = "subscribe_quotes")]
255 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
256 fn py_subscribe_quotes(
257 &mut self,
258 instrument_id: InstrumentId,
259 client_id: Option<ClientId>,
260 params: Option<IndexMap<String, String>>,
261 ) -> PyResult<()> {
262 self.subscribe_quotes(instrument_id, client_id, params);
263 Ok(())
264 }
265
266 #[pyo3(name = "subscribe_trades")]
267 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
268 fn py_subscribe_trades(
269 &mut self,
270 instrument_id: InstrumentId,
271 client_id: Option<ClientId>,
272 params: Option<IndexMap<String, String>>,
273 ) -> PyResult<()> {
274 self.subscribe_trades(instrument_id, client_id, params);
275 Ok(())
276 }
277
278 #[pyo3(name = "subscribe_bars")]
279 #[pyo3(signature = (bar_type, client_id=None, await_partial=false, params=None))]
280 fn py_subscribe_bars(
281 &mut self,
282 bar_type: BarType,
283 client_id: Option<ClientId>,
284 await_partial: bool,
285 params: Option<IndexMap<String, String>>,
286 ) -> PyResult<()> {
287 self.subscribe_bars(bar_type, client_id, await_partial, params);
288 Ok(())
289 }
290
291 #[pyo3(name = "subscribe_mark_prices")]
292 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
293 fn py_subscribe_mark_prices(
294 &mut self,
295 instrument_id: InstrumentId,
296 client_id: Option<ClientId>,
297 params: Option<IndexMap<String, String>>,
298 ) -> PyResult<()> {
299 self.subscribe_mark_prices(instrument_id, client_id, params);
300 Ok(())
301 }
302
303 #[pyo3(name = "subscribe_index_prices")]
304 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
305 fn py_subscribe_index_prices(
306 &mut self,
307 instrument_id: InstrumentId,
308 client_id: Option<ClientId>,
309 params: Option<IndexMap<String, String>>,
310 ) -> PyResult<()> {
311 self.subscribe_index_prices(instrument_id, client_id, params);
312 Ok(())
313 }
314
315 #[pyo3(name = "subscribe_instrument_status")]
316 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
317 fn py_subscribe_instrument_status(
318 &mut self,
319 instrument_id: InstrumentId,
320 client_id: Option<ClientId>,
321 params: Option<IndexMap<String, String>>,
322 ) -> PyResult<()> {
323 self.subscribe_instrument_status(instrument_id, client_id, params);
324 Ok(())
325 }
326
327 #[pyo3(name = "subscribe_instrument_close")]
328 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
329 fn py_subscribe_instrument_close(
330 &mut self,
331 instrument_id: InstrumentId,
332 client_id: Option<ClientId>,
333 params: Option<IndexMap<String, String>>,
334 ) -> PyResult<()> {
335 self.subscribe_instrument_close(instrument_id, client_id, params);
336 Ok(())
337 }
338
339 #[pyo3(name = "request_data")]
341 #[pyo3(signature = (data_type, client_id, start=None, end=None, limit=None, params=None))]
342 fn py_request_data(
343 &mut self,
344 data_type: DataType,
345 client_id: ClientId,
346 start: Option<u64>,
347 end: Option<u64>,
348 limit: Option<usize>,
349 params: Option<IndexMap<String, String>>,
350 ) -> PyResult<String> {
351 let limit = limit.and_then(NonZeroUsize::new);
352 let start = start.map(|ts| UnixNanos::from(ts).to_datetime_utc());
353 let end = end.map(|ts| UnixNanos::from(ts).to_datetime_utc());
354
355 let request_id = self
356 .request_data(data_type, client_id, start, end, limit, params)
357 .map_err(to_pyvalue_err)?;
358 Ok(request_id.to_string())
359 }
360
361 #[pyo3(name = "request_instrument")]
362 #[pyo3(signature = (instrument_id, start=None, end=None, client_id=None, params=None))]
363 fn py_request_instrument(
364 &mut self,
365 instrument_id: InstrumentId,
366 start: Option<u64>,
367 end: Option<u64>,
368 client_id: Option<ClientId>,
369 params: Option<IndexMap<String, String>>,
370 ) -> PyResult<String> {
371 let start = start.map(|ts| UnixNanos::from(ts).to_datetime_utc());
372 let end = end.map(|ts| UnixNanos::from(ts).to_datetime_utc());
373
374 let request_id = self
375 .request_instrument(instrument_id, start, end, client_id, params)
376 .map_err(to_pyvalue_err)?;
377 Ok(request_id.to_string())
378 }
379
380 #[pyo3(name = "request_instruments")]
381 #[pyo3(signature = (venue=None, start=None, end=None, client_id=None, params=None))]
382 fn py_request_instruments(
383 &mut self,
384 venue: Option<Venue>,
385 start: Option<u64>,
386 end: Option<u64>,
387 client_id: Option<ClientId>,
388 params: Option<IndexMap<String, String>>,
389 ) -> PyResult<String> {
390 let start = start.map(|ts| UnixNanos::from(ts).to_datetime_utc());
391 let end = end.map(|ts| UnixNanos::from(ts).to_datetime_utc());
392
393 let request_id = self
394 .request_instruments(venue, start, end, client_id, params)
395 .map_err(to_pyvalue_err)?;
396 Ok(request_id.to_string())
397 }
398
399 #[pyo3(name = "request_book_snapshot")]
400 #[pyo3(signature = (instrument_id, depth=None, client_id=None, params=None))]
401 fn py_request_book_snapshot(
402 &mut self,
403 instrument_id: InstrumentId,
404 depth: Option<usize>,
405 client_id: Option<ClientId>,
406 params: Option<IndexMap<String, String>>,
407 ) -> PyResult<String> {
408 let depth = depth.and_then(NonZeroUsize::new);
409
410 let request_id = self
411 .request_book_snapshot(instrument_id, depth, client_id, params)
412 .map_err(to_pyvalue_err)?;
413 Ok(request_id.to_string())
414 }
415
416 #[pyo3(name = "request_quotes")]
417 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None))]
418 fn py_request_quotes(
419 &mut self,
420 instrument_id: InstrumentId,
421 start: Option<u64>,
422 end: Option<u64>,
423 limit: Option<usize>,
424 client_id: Option<ClientId>,
425 params: Option<IndexMap<String, String>>,
426 ) -> PyResult<String> {
427 let limit = limit.and_then(NonZeroUsize::new);
428 let start = start.map(|ts| UnixNanos::from(ts).to_datetime_utc());
429 let end = end.map(|ts| UnixNanos::from(ts).to_datetime_utc());
430
431 let request_id = self
432 .request_quotes(instrument_id, start, end, limit, client_id, params)
433 .map_err(to_pyvalue_err)?;
434 Ok(request_id.to_string())
435 }
436
437 #[pyo3(name = "request_trades")]
438 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None))]
439 fn py_request_trades(
440 &mut self,
441 instrument_id: InstrumentId,
442 start: Option<u64>,
443 end: Option<u64>,
444 limit: Option<usize>,
445 client_id: Option<ClientId>,
446 params: Option<IndexMap<String, String>>,
447 ) -> PyResult<String> {
448 let limit = limit.and_then(NonZeroUsize::new);
449 let start = start.map(|ts| UnixNanos::from(ts).to_datetime_utc());
450 let end = end.map(|ts| UnixNanos::from(ts).to_datetime_utc());
451
452 let request_id = self
453 .request_trades(instrument_id, start, end, limit, client_id, params)
454 .map_err(to_pyvalue_err)?;
455 Ok(request_id.to_string())
456 }
457
458 #[pyo3(name = "request_bars")]
459 #[pyo3(signature = (bar_type, start=None, end=None, limit=None, client_id=None, params=None))]
460 fn py_request_bars(
461 &mut self,
462 bar_type: BarType,
463 start: Option<u64>,
464 end: Option<u64>,
465 limit: Option<usize>,
466 client_id: Option<ClientId>,
467 params: Option<IndexMap<String, String>>,
468 ) -> PyResult<String> {
469 let limit = limit.and_then(NonZeroUsize::new);
470 let start = start.map(|ts| UnixNanos::from(ts).to_datetime_utc());
471 let end = end.map(|ts| UnixNanos::from(ts).to_datetime_utc());
472
473 let request_id = self
474 .request_bars(bar_type, start, end, limit, client_id, params)
475 .map_err(to_pyvalue_err)?;
476 Ok(request_id.to_string())
477 }
478
479 #[pyo3(name = "unsubscribe_data")]
481 #[pyo3(signature = (data_type, client_id=None, params=None))]
482 fn py_unsubscribe_data(
483 &mut self,
484 data_type: DataType,
485 client_id: Option<ClientId>,
486 params: Option<IndexMap<String, String>>,
487 ) -> PyResult<()> {
488 self.unsubscribe_data(data_type, client_id, params);
489 Ok(())
490 }
491
492 #[pyo3(name = "unsubscribe_instruments")]
493 #[pyo3(signature = (venue, client_id=None, params=None))]
494 fn py_unsubscribe_instruments(
495 &mut self,
496 venue: Venue,
497 client_id: Option<ClientId>,
498 params: Option<IndexMap<String, String>>,
499 ) -> PyResult<()> {
500 self.unsubscribe_instruments(venue, client_id, params);
501 Ok(())
502 }
503
504 #[pyo3(name = "unsubscribe_instrument")]
505 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
506 fn py_unsubscribe_instrument(
507 &mut self,
508 instrument_id: InstrumentId,
509 client_id: Option<ClientId>,
510 params: Option<IndexMap<String, String>>,
511 ) -> PyResult<()> {
512 self.unsubscribe_instrument(instrument_id, client_id, params);
513 Ok(())
514 }
515
516 #[pyo3(name = "unsubscribe_book_deltas")]
517 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
518 fn py_unsubscribe_book_deltas(
519 &mut self,
520 instrument_id: InstrumentId,
521 client_id: Option<ClientId>,
522 params: Option<IndexMap<String, String>>,
523 ) -> PyResult<()> {
524 self.unsubscribe_book_deltas(instrument_id, client_id, params);
525 Ok(())
526 }
527
528 #[pyo3(name = "unsubscribe_book_at_interval")]
529 #[pyo3(signature = (instrument_id, interval_ms, client_id=None, params=None))]
530 fn py_unsubscribe_book_at_interval(
531 &mut self,
532 instrument_id: InstrumentId,
533 interval_ms: usize,
534 client_id: Option<ClientId>,
535 params: Option<IndexMap<String, String>>,
536 ) -> PyResult<()> {
537 let interval_ms = NonZeroUsize::new(interval_ms)
538 .ok_or_else(|| PyErr::new::<PyValueError, _>("interval_ms must be > 0"))?;
539
540 self.unsubscribe_book_at_interval(instrument_id, interval_ms, client_id, params);
541 Ok(())
542 }
543
544 #[pyo3(name = "unsubscribe_quotes")]
545 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
546 fn py_unsubscribe_quotes(
547 &mut self,
548 instrument_id: InstrumentId,
549 client_id: Option<ClientId>,
550 params: Option<IndexMap<String, String>>,
551 ) -> PyResult<()> {
552 self.unsubscribe_quotes(instrument_id, client_id, params);
553 Ok(())
554 }
555
556 #[pyo3(name = "unsubscribe_trades")]
557 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
558 fn py_unsubscribe_trades(
559 &mut self,
560 instrument_id: InstrumentId,
561 client_id: Option<ClientId>,
562 params: Option<IndexMap<String, String>>,
563 ) -> PyResult<()> {
564 self.unsubscribe_trades(instrument_id, client_id, params);
565 Ok(())
566 }
567
568 #[pyo3(name = "unsubscribe_bars")]
569 #[pyo3(signature = (bar_type, client_id=None, params=None))]
570 fn py_unsubscribe_bars(
571 &mut self,
572 bar_type: BarType,
573 client_id: Option<ClientId>,
574 params: Option<IndexMap<String, String>>,
575 ) -> PyResult<()> {
576 self.unsubscribe_bars(bar_type, client_id, params);
577 Ok(())
578 }
579
580 #[pyo3(name = "unsubscribe_mark_prices")]
581 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
582 fn py_unsubscribe_mark_prices(
583 &mut self,
584 instrument_id: InstrumentId,
585 client_id: Option<ClientId>,
586 params: Option<IndexMap<String, String>>,
587 ) -> PyResult<()> {
588 self.unsubscribe_mark_prices(instrument_id, client_id, params);
589 Ok(())
590 }
591
592 #[pyo3(name = "unsubscribe_index_prices")]
593 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
594 fn py_unsubscribe_index_prices(
595 &mut self,
596 instrument_id: InstrumentId,
597 client_id: Option<ClientId>,
598 params: Option<IndexMap<String, String>>,
599 ) -> PyResult<()> {
600 self.unsubscribe_index_prices(instrument_id, client_id, params);
601 Ok(())
602 }
603
604 #[pyo3(name = "unsubscribe_instrument_status")]
605 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
606 fn py_unsubscribe_instrument_status(
607 &mut self,
608 instrument_id: InstrumentId,
609 client_id: Option<ClientId>,
610 params: Option<IndexMap<String, String>>,
611 ) -> PyResult<()> {
612 self.unsubscribe_instrument_status(instrument_id, client_id, params);
613 Ok(())
614 }
615
616 #[pyo3(name = "unsubscribe_instrument_close")]
617 #[pyo3(signature = (instrument_id, client_id=None, params=None))]
618 fn py_unsubscribe_instrument_close(
619 &mut self,
620 instrument_id: InstrumentId,
621 client_id: Option<ClientId>,
622 params: Option<IndexMap<String, String>>,
623 ) -> PyResult<()> {
624 self.unsubscribe_instrument_close(instrument_id, client_id, params);
625 Ok(())
626 }
627}
628
629#[cfg(test)]
633mod tests {
634 use std::{cell::RefCell, rc::Rc, str::FromStr, sync::Arc};
635
636 use nautilus_model::{
637 data::{BarType, DataType},
638 enums::BookType,
639 identifiers::{ClientId, TraderId, Venue},
640 instruments::{CurrencyPair, stubs::audusd_sim},
641 };
642 use rstest::{fixture, rstest};
643
644 use super::PyDataActor;
645 use crate::{
646 cache::Cache,
647 clock::TestClock,
648 component::Component,
649 enums::ComponentState,
650 runner::{SyncDataCommandSender, set_data_cmd_sender},
651 };
652
653 #[fixture]
654 fn clock() -> Rc<RefCell<TestClock>> {
655 Rc::new(RefCell::new(TestClock::new()))
656 }
657
658 #[fixture]
659 fn cache() -> Rc<RefCell<Cache>> {
660 Rc::new(RefCell::new(Cache::new(None, None)))
661 }
662
663 #[fixture]
664 fn trader_id() -> TraderId {
665 TraderId::from("TRADER-001")
666 }
667
668 #[fixture]
669 fn client_id() -> ClientId {
670 ClientId::new("TestClient")
671 }
672
673 #[fixture]
674 fn venue() -> Venue {
675 Venue::from("SIM")
676 }
677
678 #[fixture]
679 fn data_type() -> DataType {
680 DataType::new("TestData", None)
681 }
682
683 #[fixture]
684 fn bar_type(audusd_sim: CurrencyPair) -> BarType {
685 BarType::from_str(&format!("{}-1-MINUTE-LAST-INTERNAL", audusd_sim.id)).unwrap()
686 }
687
688 fn create_unregistered_actor() -> PyDataActor {
689 PyDataActor::py_new(None).unwrap()
690 }
691
692 fn create_registered_actor(
693 clock: Rc<RefCell<TestClock>>,
694 cache: Rc<RefCell<Cache>>,
695 trader_id: TraderId,
696 ) -> PyDataActor {
697 let sender = SyncDataCommandSender;
699 set_data_cmd_sender(Arc::new(sender));
700
701 let mut actor = PyDataActor::py_new(None).unwrap();
702 actor.register(trader_id, clock, cache).unwrap();
703 actor
704 }
705
706 #[rstest]
707 fn test_new_actor_creation() {
708 let actor = PyDataActor::py_new(None).unwrap();
709 assert!(actor.trader_id().is_none());
710 }
711
712 #[rstest]
713 fn test_unregistered_actor_methods_work(data_type: DataType, client_id: ClientId) {
714 let actor = create_unregistered_actor();
715
716 assert!(!actor.py_is_ready());
717 assert!(!actor.py_is_running());
718 assert!(!actor.py_is_stopped());
719 assert!(!actor.py_is_disposed());
720 assert!(!actor.py_is_degraded());
721 assert!(!actor.py_is_faulted());
722
723 assert_eq!(actor.trader_id(), None);
725 }
726
727 #[rstest]
728 fn test_registration_success(
729 clock: Rc<RefCell<TestClock>>,
730 cache: Rc<RefCell<Cache>>,
731 trader_id: TraderId,
732 ) {
733 let mut actor = create_unregistered_actor();
734 actor.register(trader_id, clock, cache).unwrap();
735 assert!(actor.trader_id().is_some());
736 assert_eq!(actor.trader_id().unwrap(), trader_id);
737 }
738
739 #[rstest]
740 fn test_registered_actor_basic_properties(
741 clock: Rc<RefCell<TestClock>>,
742 cache: Rc<RefCell<Cache>>,
743 trader_id: TraderId,
744 ) {
745 let actor = create_registered_actor(clock, cache, trader_id);
746
747 assert_eq!(actor.state(), ComponentState::Ready);
748 assert_eq!(actor.trader_id(), Some(TraderId::from("TRADER-001")));
749 assert!(actor.py_is_ready());
750 assert!(!actor.py_is_running());
751 assert!(!actor.py_is_stopped());
752 assert!(!actor.py_is_disposed());
753 assert!(!actor.py_is_degraded());
754 assert!(!actor.py_is_faulted());
755 }
756
757 #[rstest]
758 fn test_basic_subscription_methods_compile(
759 clock: Rc<RefCell<TestClock>>,
760 cache: Rc<RefCell<Cache>>,
761 trader_id: TraderId,
762 data_type: DataType,
763 client_id: ClientId,
764 audusd_sim: CurrencyPair,
765 ) {
766 let mut actor = create_registered_actor(clock, cache, trader_id);
767
768 let _ = actor.py_subscribe_data(data_type.clone(), Some(client_id.clone()), None);
769 let _ = actor.py_subscribe_quotes(audusd_sim.id, Some(client_id.clone()), None);
770 let _ = actor.py_unsubscribe_data(data_type, Some(client_id.clone()), None);
771 let _ = actor.py_unsubscribe_quotes(audusd_sim.id, Some(client_id), None);
772 }
773
774 #[rstest]
775 fn test_lifecycle_methods_pass_through(
776 clock: Rc<RefCell<TestClock>>,
777 cache: Rc<RefCell<Cache>>,
778 trader_id: TraderId,
779 ) {
780 let mut actor = create_registered_actor(clock, cache, trader_id);
781
782 assert!(actor.py_start().is_ok());
783 assert!(actor.py_stop().is_ok());
784 assert!(actor.py_dispose().is_ok());
785 }
786
787 #[rstest]
788 fn test_shutdown_system_passes_through(
789 clock: Rc<RefCell<TestClock>>,
790 cache: Rc<RefCell<Cache>>,
791 trader_id: TraderId,
792 ) {
793 let actor = create_registered_actor(clock, cache, trader_id);
794
795 assert!(
796 actor
797 .py_shutdown_system(Some("Test shutdown".to_string()))
798 .is_ok()
799 );
800 assert!(actor.py_shutdown_system(None).is_ok());
801 }
802
803 #[rstest]
804 fn test_book_at_interval_invalid_interval_ms(
805 clock: Rc<RefCell<TestClock>>,
806 cache: Rc<RefCell<Cache>>,
807 trader_id: TraderId,
808 audusd_sim: CurrencyPair,
809 ) {
810 pyo3::prepare_freethreaded_python();
811
812 let mut actor = create_registered_actor(clock, cache, trader_id);
813
814 let result = actor.py_subscribe_book_at_interval(
815 audusd_sim.id,
816 BookType::L2_MBP,
817 0,
818 None,
819 None,
820 None,
821 );
822 assert!(result.is_err());
823 assert_eq!(
824 result.unwrap_err().to_string(),
825 "ValueError: interval_ms must be > 0"
826 );
827
828 let result = actor.py_unsubscribe_book_at_interval(audusd_sim.id, 0, None, None);
829 assert!(result.is_err());
830 assert_eq!(
831 result.unwrap_err().to_string(),
832 "ValueError: interval_ms must be > 0"
833 );
834 }
835
836 #[rstest]
837 fn test_request_methods_signatures_exist() {
838 let actor = create_unregistered_actor();
839
840 assert!(actor.trader_id().is_none());
843 }
844
845 #[rstest]
846 fn test_data_actor_trait_implementation(
847 clock: Rc<RefCell<TestClock>>,
848 cache: Rc<RefCell<Cache>>,
849 trader_id: TraderId,
850 ) {
851 let actor = create_registered_actor(clock, cache, trader_id);
852
853 let state = actor.state();
855 assert_eq!(state, ComponentState::Ready);
856 }
857}