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