nautilus_common/python/
clock.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.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
16use std::{cell::RefCell, rc::Rc};
17
18use chrono::{DateTime, Duration, Utc};
19use nautilus_core::{UnixNanos, python::to_pyvalue_err};
20use pyo3::prelude::*;
21
22use crate::{
23    clock::{Clock, TestClock},
24    live::clock::LiveClock,
25    timer::TimeEventCallback,
26};
27
28/// Unified PyO3 interface over both [`TestClock`] and [`LiveClock`].
29///
30/// A `PyClock` instance owns a boxed trait object implementing [`Clock`].  It
31/// delegates method calls to this inner clock, allowing a single Python class
32/// to transparently wrap either implementation and eliminating the large
33/// amount of duplicated glue code previously required.
34///
35/// It intentionally does **not** expose a `__new__` constructor to Python –
36/// clocks should be created from Rust and handed over to Python as needed.
37#[allow(non_camel_case_types)]
38#[pyo3::pyclass(
39    module = "nautilus_trader.core.nautilus_pyo3.common",
40    name = "Clock",
41    unsendable
42)]
43#[derive(Debug, Clone)]
44pub struct PyClock(Rc<RefCell<dyn Clock>>);
45
46#[pymethods]
47impl PyClock {
48    #[pyo3(name = "register_default_handler")]
49    fn py_register_default_handler(&mut self, callback: Py<PyAny>) {
50        self.0
51            .borrow_mut()
52            .register_default_handler(TimeEventCallback::from(callback));
53    }
54
55    #[pyo3(
56        name = "set_time_alert",
57        signature = (name, alert_time, callback=None, allow_past=None)
58    )]
59    fn py_set_time_alert(
60        &mut self,
61        name: &str,
62        alert_time: DateTime<Utc>,
63        callback: Option<Py<PyAny>>,
64        allow_past: Option<bool>,
65    ) -> PyResult<()> {
66        self.0
67            .borrow_mut()
68            .set_time_alert(
69                name,
70                alert_time,
71                callback.map(TimeEventCallback::from),
72                allow_past,
73            )
74            .map_err(to_pyvalue_err)
75    }
76
77    #[pyo3(
78        name = "set_time_alert_ns",
79        signature = (name, alert_time_ns, callback=None, allow_past=None)
80    )]
81    fn py_set_time_alert_ns(
82        &mut self,
83        name: &str,
84        alert_time_ns: u64,
85        callback: Option<Py<PyAny>>,
86        allow_past: Option<bool>,
87    ) -> PyResult<()> {
88        self.0
89            .borrow_mut()
90            .set_time_alert_ns(
91                name,
92                alert_time_ns.into(),
93                callback.map(TimeEventCallback::from),
94                allow_past,
95            )
96            .map_err(to_pyvalue_err)
97    }
98
99    #[allow(clippy::too_many_arguments)]
100    #[pyo3(
101        name = "set_timer",
102        signature = (name, interval, start_time=None, stop_time=None, callback=None, allow_past=None, fire_immediately=None)
103    )]
104    fn py_set_timer(
105        &mut self,
106        name: &str,
107        interval: Duration,
108        start_time: Option<DateTime<Utc>>,
109        stop_time: Option<DateTime<Utc>>,
110        callback: Option<Py<PyAny>>,
111        allow_past: Option<bool>,
112        fire_immediately: Option<bool>,
113    ) -> PyResult<()> {
114        let interval_ns_i64 = interval
115            .num_nanoseconds()
116            .ok_or_else(|| pyo3::exceptions::PyValueError::new_err("Interval too large"))?;
117        if interval_ns_i64 <= 0 {
118            return Err(pyo3::exceptions::PyValueError::new_err(
119                "Interval must be positive",
120            ));
121        }
122        let interval_ns = interval_ns_i64 as u64;
123
124        self.0
125            .borrow_mut()
126            .set_timer_ns(
127                name,
128                interval_ns,
129                start_time.map(UnixNanos::from),
130                stop_time.map(UnixNanos::from),
131                callback.map(TimeEventCallback::from),
132                allow_past,
133                fire_immediately,
134            )
135            .map_err(to_pyvalue_err)
136    }
137
138    #[allow(clippy::too_many_arguments)]
139    #[pyo3(
140        name = "set_timer_ns",
141        signature = (name, interval_ns, start_time_ns=None, stop_time_ns=None, callback=None, allow_past=None, fire_immediately=None)
142    )]
143    fn py_set_timer_ns(
144        &mut self,
145        name: &str,
146        interval_ns: u64,
147        start_time_ns: Option<u64>,
148        stop_time_ns: Option<u64>,
149        callback: Option<Py<PyAny>>,
150        allow_past: Option<bool>,
151        fire_immediately: Option<bool>,
152    ) -> PyResult<()> {
153        self.0
154            .borrow_mut()
155            .set_timer_ns(
156                name,
157                interval_ns,
158                start_time_ns.map(UnixNanos::from),
159                stop_time_ns.map(UnixNanos::from),
160                callback.map(TimeEventCallback::from),
161                allow_past,
162                fire_immediately,
163            )
164            .map_err(to_pyvalue_err)
165    }
166
167    #[pyo3(name = "next_time_ns")]
168    fn py_next_time_ns(&self, name: &str) -> Option<u64> {
169        self.0.borrow().next_time_ns(name).map(|t| t.as_u64())
170    }
171
172    #[pyo3(name = "cancel_timer")]
173    fn py_cancel_timer(&mut self, name: &str) {
174        self.0.borrow_mut().cancel_timer(name);
175    }
176
177    #[pyo3(name = "cancel_timers")]
178    fn py_cancel_timers(&mut self) {
179        self.0.borrow_mut().cancel_timers();
180    }
181}
182
183impl PyClock {
184    /// Creates a `PyClock` directly from an `Rc<RefCell<dyn Clock>>`.
185    #[must_use]
186    pub fn from_rc(rc: Rc<RefCell<dyn Clock>>) -> Self {
187        Self(rc)
188    }
189
190    /// Creates a clock backed by [`TestClock`].
191    #[must_use]
192    pub fn new_test() -> Self {
193        Self(Rc::new(RefCell::new(TestClock::default())))
194    }
195
196    /// Creates a clock backed by [`LiveClock`].
197    #[must_use]
198    pub fn new_live() -> Self {
199        Self(Rc::new(RefCell::new(LiveClock::default())))
200    }
201
202    /// Provides access to the inner [`Clock`] trait object.
203    #[must_use]
204    pub fn inner(&self) -> std::cell::Ref<'_, dyn Clock> {
205        self.0.borrow()
206    }
207
208    /// Mutably accesses the underlying [`Clock`].
209    #[must_use]
210    pub fn inner_mut(&mut self) -> std::cell::RefMut<'_, dyn Clock> {
211        self.0.borrow_mut()
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use std::sync::Arc;
218
219    use chrono::{Duration, Utc};
220    use nautilus_core::{UnixNanos, python::IntoPyObjectNautilusExt};
221    use pyo3::{prelude::*, types::PyList};
222    use rstest::*;
223
224    use crate::{
225        clock::{Clock, TestClock},
226        python::clock::PyClock,
227        runner::{TimeEventSender, set_time_event_sender},
228        timer::{TimeEventCallback, TimeEventHandlerV2},
229    };
230
231    fn ensure_sender() {
232        if crate::runner::try_get_time_event_sender().is_none() {
233            set_time_event_sender(Arc::new(DummySender));
234        }
235    }
236
237    // Dummy TimeEventSender for LiveClock tests
238    #[derive(Debug)]
239    struct DummySender;
240
241    impl TimeEventSender for DummySender {
242        fn send(&self, _handler: TimeEventHandlerV2) {}
243    }
244
245    #[fixture]
246    pub fn test_clock() -> TestClock {
247        TestClock::new()
248    }
249
250    pub fn test_callback() -> TimeEventCallback {
251        Python::initialize();
252        Python::attach(|py| {
253            let py_list = PyList::empty(py);
254            let py_append = Py::from(py_list.getattr("append").unwrap());
255            let py_append = py_append.into_py_any_unwrap(py);
256            TimeEventCallback::from(py_append)
257        })
258    }
259
260    pub fn test_py_callback() -> Py<PyAny> {
261        Python::initialize();
262        Python::attach(|py| {
263            let py_list = PyList::empty(py);
264            let py_append = Py::from(py_list.getattr("append").unwrap());
265            py_append.into_py_any_unwrap(py)
266        })
267    }
268
269    ////////////////////////////////////////////////////////////////////////////////
270    // TestClock_Py
271    ////////////////////////////////////////////////////////////////////////////////
272
273    #[rstest]
274    fn test_test_clock_py_set_time_alert() {
275        Python::initialize();
276        Python::attach(|_py| {
277            let mut py_clock = PyClock::new_test();
278            let callback = test_py_callback();
279            py_clock.py_register_default_handler(callback);
280            let dt = Utc::now() + Duration::seconds(1);
281            py_clock
282                .py_set_time_alert("ALERT1", dt, None, None)
283                .expect("set_time_alert failed");
284        });
285    }
286
287    #[rstest]
288    fn test_test_clock_py_set_timer() {
289        Python::initialize();
290        Python::attach(|_py| {
291            let mut py_clock = PyClock::new_test();
292            let callback = test_py_callback();
293            py_clock.py_register_default_handler(callback);
294            let interval = Duration::seconds(2);
295            py_clock
296                .py_set_timer("TIMER1", interval, None, None, None, None, None)
297                .expect("set_timer failed");
298        });
299    }
300
301    #[rstest]
302    fn test_test_clock_py_set_time_alert_ns() {
303        Python::initialize();
304        Python::attach(|_py| {
305            let mut py_clock = PyClock::new_test();
306            let callback = test_py_callback();
307            py_clock.py_register_default_handler(callback);
308            let ts_ns = (Utc::now() + Duration::seconds(1))
309                .timestamp_nanos_opt()
310                .unwrap() as u64;
311            py_clock
312                .py_set_time_alert_ns("ALERT_NS", ts_ns, None, None)
313                .expect("set_time_alert_ns failed");
314        });
315    }
316
317    #[rstest]
318    fn test_test_clock_py_set_timer_ns() {
319        Python::initialize();
320        Python::attach(|_py| {
321            let mut py_clock = PyClock::new_test();
322            let callback = test_py_callback();
323            py_clock.py_register_default_handler(callback);
324            py_clock
325                .py_set_timer_ns("TIMER_NS", 1_000_000, None, None, None, None, None)
326                .expect("set_timer_ns failed");
327        });
328    }
329
330    #[rstest]
331    fn test_test_clock_raw_set_timer_ns(mut test_clock: TestClock) {
332        Python::initialize();
333        Python::attach(|_py| {
334            let callback = test_callback();
335            test_clock.register_default_handler(callback);
336
337            let timer_name = "TEST_TIME1";
338            test_clock
339                .set_timer_ns(timer_name, 10, None, None, None, None, None)
340                .unwrap();
341
342            assert_eq!(test_clock.timer_names(), [timer_name]);
343            assert_eq!(test_clock.timer_count(), 1);
344        });
345    }
346
347    #[rstest]
348    fn test_test_clock_cancel_timer(mut test_clock: TestClock) {
349        Python::initialize();
350        Python::attach(|_py| {
351            let callback = test_callback();
352            test_clock.register_default_handler(callback);
353
354            let timer_name = "TEST_TIME1";
355            test_clock
356                .set_timer_ns(timer_name, 10, None, None, None, None, None)
357                .unwrap();
358            test_clock.cancel_timer(timer_name);
359
360            assert!(test_clock.timer_names().is_empty());
361            assert_eq!(test_clock.timer_count(), 0);
362        });
363    }
364
365    #[rstest]
366    fn test_test_clock_cancel_timers(mut test_clock: TestClock) {
367        Python::initialize();
368        Python::attach(|_py| {
369            let callback = test_callback();
370            test_clock.register_default_handler(callback);
371
372            let timer_name = "TEST_TIME1";
373            test_clock
374                .set_timer_ns(timer_name, 10, None, None, None, None, None)
375                .unwrap();
376            test_clock.cancel_timers();
377
378            assert!(test_clock.timer_names().is_empty());
379            assert_eq!(test_clock.timer_count(), 0);
380        });
381    }
382
383    #[rstest]
384    fn test_test_clock_advance_within_stop_time_py(mut test_clock: TestClock) {
385        Python::initialize();
386        Python::attach(|_py| {
387            let callback = test_callback();
388            test_clock.register_default_handler(callback);
389
390            let timer_name = "TEST_TIME1";
391            test_clock
392                .set_timer_ns(
393                    timer_name,
394                    1,
395                    Some(UnixNanos::from(1)),
396                    Some(UnixNanos::from(3)),
397                    None,
398                    None,
399                    None,
400                )
401                .unwrap();
402            test_clock.advance_time(2.into(), true);
403
404            assert_eq!(test_clock.timer_names(), [timer_name]);
405            assert_eq!(test_clock.timer_count(), 1);
406        });
407    }
408
409    #[rstest]
410    fn test_test_clock_advance_time_to_stop_time_with_set_time_true(mut test_clock: TestClock) {
411        Python::initialize();
412        Python::attach(|_py| {
413            let callback = test_callback();
414            test_clock.register_default_handler(callback);
415
416            test_clock
417                .set_timer_ns(
418                    "TEST_TIME1",
419                    2,
420                    None,
421                    Some(UnixNanos::from(3)),
422                    None,
423                    None,
424                    None,
425                )
426                .unwrap();
427            test_clock.advance_time(3.into(), true);
428
429            assert_eq!(test_clock.timer_names().len(), 1);
430            assert_eq!(test_clock.timer_count(), 1);
431            assert_eq!(test_clock.get_time_ns(), 3);
432        });
433    }
434
435    #[rstest]
436    fn test_test_clock_advance_time_to_stop_time_with_set_time_false(mut test_clock: TestClock) {
437        Python::initialize();
438        Python::attach(|_py| {
439            let callback = test_callback();
440            test_clock.register_default_handler(callback);
441
442            test_clock
443                .set_timer_ns(
444                    "TEST_TIME1",
445                    2,
446                    None,
447                    Some(UnixNanos::from(3)),
448                    None,
449                    None,
450                    None,
451                )
452                .unwrap();
453            test_clock.advance_time(3.into(), false);
454
455            assert_eq!(test_clock.timer_names().len(), 1);
456            assert_eq!(test_clock.timer_count(), 1);
457            assert_eq!(test_clock.get_time_ns(), 0);
458        });
459    }
460
461    ////////////////////////////////////////////////////////////////////////////////
462    // LiveClock_Py
463    ////////////////////////////////////////////////////////////////////////////////
464
465    #[rstest]
466    fn test_live_clock_py_set_time_alert() {
467        ensure_sender();
468
469        Python::initialize();
470        Python::attach(|_py| {
471            let mut py_clock = PyClock::new_live();
472            let callback = test_py_callback();
473            py_clock.py_register_default_handler(callback);
474            let dt = Utc::now() + Duration::seconds(1);
475
476            py_clock
477                .py_set_time_alert("ALERT1", dt, None, None)
478                .expect("live set_time_alert failed");
479        });
480    }
481
482    #[rstest]
483    fn test_live_clock_py_set_timer() {
484        ensure_sender();
485
486        Python::initialize();
487        Python::attach(|_py| {
488            let mut py_clock = PyClock::new_live();
489            let callback = test_py_callback();
490            py_clock.py_register_default_handler(callback);
491            let interval = Duration::seconds(3);
492
493            py_clock
494                .py_set_timer("TIMER1", interval, None, None, None, None, None)
495                .expect("live set_timer failed");
496        });
497    }
498
499    #[rstest]
500    fn test_live_clock_py_set_time_alert_ns() {
501        ensure_sender();
502
503        Python::initialize();
504        Python::attach(|_py| {
505            let mut py_clock = PyClock::new_live();
506            let callback = test_py_callback();
507            py_clock.py_register_default_handler(callback);
508            let dt_ns = (Utc::now() + Duration::seconds(1))
509                .timestamp_nanos_opt()
510                .unwrap() as u64;
511
512            py_clock
513                .py_set_time_alert_ns("ALERT_NS", dt_ns, None, None)
514                .expect("live set_time_alert_ns failed");
515        });
516    }
517
518    #[rstest]
519    fn test_live_clock_py_set_timer_ns() {
520        ensure_sender();
521
522        Python::initialize();
523        Python::attach(|_py| {
524            let mut py_clock = PyClock::new_live();
525            let callback = test_py_callback();
526            py_clock.py_register_default_handler(callback);
527            let interval_ns = 1_000_000_000_u64; // 1 second
528
529            py_clock
530                .py_set_timer_ns("TIMER_NS", interval_ns, None, None, None, None, None)
531                .expect("live set_timer_ns failed");
532        });
533    }
534}