nautilus_common/python/
actor.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16// Under development
17#![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        // TODO: Parse config from Python if provided
80        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    // Request methods
340    #[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    // Unsubscribe methods
480    #[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////////////////////////////////////////////////////////////////////////////////
630// Tests
631////////////////////////////////////////////////////////////////////////////////
632#[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        // Set up sync data command sender for tests
698        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        // Verify unregistered state
724        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        // These methods exist and compile correctly
841        // Verify it's unregistered
842        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        // Test Component trait method (using the trait method directly)
854        let state = actor.state();
855        assert_eq!(state, ComponentState::Ready);
856    }
857}