Skip to main content

nautilus_common/python/
clock.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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
16#![warn(clippy::clone_on_ref_ptr)]
17
18use std::{cell::RefCell, rc::Rc};
19
20use jiff::{SignedDuration, Timestamp};
21use nautilus_core::{
22    DurationNanos, UnixNanos, datetime::try_datetime_to_unix_nanos, python::to_pyvalue_err,
23};
24use pyo3::prelude::*;
25
26use crate::{
27    clock::{Clock, VirtualClock},
28    live::clock::LiveClock,
29    timer::TimeEventCallback,
30};
31
32/// Unified PyO3 interface over both [`VirtualClock`] and [`LiveClock`].
33///
34/// A `PyClock` instance owns a boxed trait object implementing [`Clock`].  It
35/// delegates method calls to this inner clock, allowing a single Python class
36/// to transparently wrap either implementation and eliminating the large
37/// amount of duplicated glue code previously required.
38///
39/// It intentionally does **not** expose a `__new__` constructor to Python -
40/// clocks should be created from Rust and handed over to Python as needed.
41#[allow(non_camel_case_types)]
42#[pyo3::pyclass(
43    module = "nautilus_trader.common",
44    name = "Clock",
45    unsendable,
46    from_py_object
47)]
48#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")]
49#[derive(Debug, Clone)]
50pub struct PyClock(Rc<RefCell<dyn Clock>>);
51
52#[pymethods]
53#[pyo3_stub_gen::derive::gen_stub_pymethods]
54impl PyClock {
55    #[staticmethod]
56    #[pyo3(name = "new_test")]
57    fn py_new_test() -> Self {
58        Self(Rc::new(RefCell::new(VirtualClock::default())))
59    }
60
61    /// Returns the current UNIX timestamp in nanoseconds (ns).
62    #[pyo3(name = "timestamp_ns")]
63    fn py_timestamp_ns(&self) -> u64 {
64        self.0.borrow().timestamp_ns().as_u64()
65    }
66
67    /// Returns the current UNIX timestamp in microseconds (μs).
68    #[pyo3(name = "timestamp_us")]
69    fn py_timestamp_us(&self) -> u64 {
70        self.0.borrow().timestamp_us()
71    }
72
73    /// Returns the current UNIX timestamp in milliseconds (ms).
74    #[pyo3(name = "timestamp_ms")]
75    fn py_timestamp_ms(&self) -> u64 {
76        self.0.borrow().timestamp_ms()
77    }
78
79    /// Returns the current UNIX timestamp in seconds.
80    #[pyo3(name = "timestamp")]
81    fn py_timestamp(&self) -> f64 {
82        self.0.borrow().timestamp()
83    }
84
85    /// Returns the current UTC timestamp.
86    #[pyo3(name = "utc_now")]
87    fn py_utc_now(&self) -> Timestamp {
88        self.0.borrow().utc_now()
89    }
90
91    #[pyo3(name = "set_time")]
92    fn py_set_time(&mut self, to_time_ns: u64) -> PyResult<()> {
93        let mut clock = self.0.borrow_mut();
94        let Some(test_clock) = clock.as_any_mut().downcast_mut::<VirtualClock>() else {
95            return Err(to_pyvalue_err(
96                "set_time is only supported by virtual clocks",
97            ));
98        };
99
100        test_clock.set_time(to_time_ns.into());
101        Ok(())
102    }
103
104    /// Returns the names of active timers in the clock.
105    #[pyo3(name = "timer_names")]
106    fn py_timer_names(&self) -> Vec<String> {
107        self.0
108            .borrow()
109            .timer_names()
110            .into_iter()
111            .map(String::from)
112            .collect()
113    }
114
115    /// Returns the count of active timers in the clock.
116    #[pyo3(name = "timer_count")]
117    fn py_timer_count(&self) -> usize {
118        self.0.borrow().timer_count()
119    }
120
121    #[pyo3(name = "register_default_handler")]
122    fn py_register_default_handler(&mut self, callback: Py<PyAny>) {
123        self.0
124            .borrow_mut()
125            .register_default_handler(TimeEventCallback::from(callback));
126    }
127
128    #[pyo3(name = "cancel_default_handler")]
129    fn py_cancel_default_handler(&mut self) {
130        self.0.borrow_mut().cancel_default_handler();
131    }
132
133    #[pyo3(name = "cancel_callbacks")]
134    fn py_cancel_callbacks(&mut self) {
135        self.0.borrow_mut().cancel_callbacks();
136    }
137
138    #[pyo3(
139        name = "set_time_alert",
140        signature = (name, alert_time, callback=None, allow_past=None)
141    )]
142    fn py_set_time_alert(
143        &mut self,
144        name: &str,
145        alert_time: Timestamp,
146        callback: Option<Py<PyAny>>,
147        allow_past: Option<bool>,
148    ) -> PyResult<()> {
149        self.0
150            .borrow_mut()
151            .set_time_alert(
152                name,
153                alert_time,
154                callback.map(TimeEventCallback::from),
155                allow_past,
156            )
157            .map_err(to_pyvalue_err)
158    }
159
160    #[pyo3(
161        name = "set_time_alert_ns",
162        signature = (name, alert_time_ns, callback=None, allow_past=None)
163    )]
164    fn py_set_time_alert_ns(
165        &mut self,
166        name: &str,
167        alert_time_ns: u64,
168        callback: Option<Py<PyAny>>,
169        allow_past: Option<bool>,
170    ) -> PyResult<()> {
171        self.0
172            .borrow_mut()
173            .set_time_alert_ns(
174                name,
175                alert_time_ns.into(),
176                callback.map(TimeEventCallback::from),
177                allow_past,
178            )
179            .map_err(to_pyvalue_err)
180    }
181
182    #[expect(clippy::too_many_arguments)]
183    #[pyo3(
184        name = "set_timer",
185        signature = (name, interval, start_time=None, stop_time=None, callback=None, allow_past=None, fire_immediately=None)
186    )]
187    fn py_set_timer(
188        &mut self,
189        name: &str,
190        interval: SignedDuration,
191        start_time: Option<Timestamp>,
192        stop_time: Option<Timestamp>,
193        callback: Option<Py<PyAny>>,
194        allow_past: Option<bool>,
195        fire_immediately: Option<bool>,
196    ) -> PyResult<()> {
197        if interval <= SignedDuration::ZERO {
198            return Err(to_pyvalue_err("Interval must be positive"));
199        }
200        let interval_ns =
201            DurationNanos::try_from(interval).map_err(|_| to_pyvalue_err("Interval too large"))?;
202
203        let start_time_ns = start_time
204            .map(try_datetime_to_unix_nanos)
205            .transpose()
206            .map_err(to_pyvalue_err)?;
207        let stop_time_ns = stop_time
208            .map(try_datetime_to_unix_nanos)
209            .transpose()
210            .map_err(to_pyvalue_err)?;
211
212        self.0
213            .borrow_mut()
214            .set_timer_ns(
215                name,
216                interval_ns,
217                start_time_ns,
218                stop_time_ns,
219                callback.map(TimeEventCallback::from),
220                allow_past,
221                fire_immediately,
222            )
223            .map_err(to_pyvalue_err)
224    }
225
226    #[expect(clippy::too_many_arguments)]
227    #[pyo3(
228        name = "set_timer_ns",
229        signature = (name, interval_ns, start_time_ns=None, stop_time_ns=None, callback=None, allow_past=None, fire_immediately=None)
230    )]
231    fn py_set_timer_ns(
232        &mut self,
233        name: &str,
234        interval_ns: u64,
235        start_time_ns: Option<u64>,
236        stop_time_ns: Option<u64>,
237        callback: Option<Py<PyAny>>,
238        allow_past: Option<bool>,
239        fire_immediately: Option<bool>,
240    ) -> PyResult<()> {
241        self.0
242            .borrow_mut()
243            .set_timer_ns(
244                name,
245                DurationNanos::new(interval_ns),
246                start_time_ns.map(UnixNanos::from),
247                stop_time_ns.map(UnixNanos::from),
248                callback.map(TimeEventCallback::from),
249                allow_past,
250                fire_immediately,
251            )
252            .map_err(to_pyvalue_err)
253    }
254
255    #[pyo3(name = "next_time_ns")]
256    fn py_next_time_ns(&self, name: &str) -> Option<u64> {
257        self.0.borrow().next_time_ns(name).map(|t| t.as_u64())
258    }
259
260    #[pyo3(name = "cancel_timer")]
261    fn py_cancel_timer(&mut self, name: &str) {
262        self.0.borrow_mut().cancel_timer(name);
263    }
264
265    #[pyo3(name = "cancel_timers")]
266    fn py_cancel_timers(&mut self) {
267        self.0.borrow_mut().cancel_timers();
268    }
269}
270
271impl PyClock {
272    /// Creates a `PyClock` directly from an `Rc<RefCell<dyn Clock>>`.
273    #[must_use]
274    pub fn from_rc(rc: Rc<RefCell<dyn Clock>>) -> Self {
275        Self(rc)
276    }
277
278    /// Gets the inner `Rc<RefCell<dyn Clock>>` for use in Rust code.
279    #[must_use]
280    pub fn clock_rc(&self) -> Rc<RefCell<dyn Clock>> {
281        Rc::clone(&self.0)
282    }
283
284    /// Creates a clock backed by [`VirtualClock`].
285    #[must_use]
286    pub fn new_test() -> Self {
287        Self(Rc::new(RefCell::new(VirtualClock::default())))
288    }
289
290    /// Creates a clock backed by [`LiveClock`].
291    #[must_use]
292    pub fn new_live() -> Self {
293        Self(Rc::new(RefCell::new(LiveClock::default())))
294    }
295
296    /// Provides access to the inner [`Clock`] trait object.
297    #[must_use]
298    pub fn inner(&self) -> std::cell::Ref<'_, dyn Clock> {
299        self.0.borrow()
300    }
301
302    /// Mutably accesses the underlying [`Clock`].
303    #[must_use]
304    pub fn inner_mut(&mut self) -> std::cell::RefMut<'_, dyn Clock> {
305        self.0.borrow_mut()
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use std::sync::Arc;
312
313    use jiff::{SignedDuration, Timestamp};
314    use nautilus_core::{DurationNanos, UnixNanos, python::IntoPyObjectNautilusExt};
315    use pyo3::{prelude::*, types::PyList};
316    use rstest::*;
317
318    use crate::{
319        clock::{Clock, VirtualClock},
320        python::clock::PyClock,
321        runner::{TimeEventMessage, TimeEventSender, set_time_event_sender},
322        timer::TimeEventCallback,
323    };
324
325    fn ensure_sender() {
326        if crate::runner::try_get_time_event_sender().is_none() {
327            set_time_event_sender(Arc::new(DummySender));
328        }
329    }
330
331    // Dummy TimeEventSender for LiveClock tests
332    #[derive(Debug)]
333    struct DummySender;
334
335    impl TimeEventSender for DummySender {
336        fn send(&self, _message: TimeEventMessage) {}
337    }
338
339    #[fixture]
340    pub fn test_clock() -> VirtualClock {
341        VirtualClock::new()
342    }
343
344    pub(super) fn test_callback() -> TimeEventCallback {
345        Python::initialize();
346        Python::attach(|py| {
347            let py_list = PyList::empty(py);
348            let py_append = Py::from(py_list.getattr("append").unwrap());
349            let py_append = py_append.into_py_any_unwrap(py);
350            TimeEventCallback::from(py_append)
351        })
352    }
353
354    pub(super) fn test_py_callback() -> Py<PyAny> {
355        Python::initialize();
356        Python::attach(|py| {
357            let py_list = PyList::empty(py);
358            let py_append = Py::from(py_list.getattr("append").unwrap());
359            py_append.into_py_any_unwrap(py)
360        })
361    }
362
363    ////////////////////////////////////////////////////////////////////////////////
364    // VirtualClock_Py
365    ////////////////////////////////////////////////////////////////////////////////
366
367    #[rstest]
368    fn test_test_clock_py_set_time_alert() {
369        Python::initialize();
370        Python::attach(|_py| {
371            let mut py_clock = PyClock::new_test();
372            let callback = test_py_callback();
373            py_clock.py_register_default_handler(callback);
374            let dt = Timestamp::now() + SignedDuration::from_secs(1);
375            py_clock
376                .py_set_time_alert("ALERT1", dt, None, None)
377                .expect("set_time_alert failed");
378        });
379    }
380
381    #[rstest]
382    fn test_test_clock_py_set_time() {
383        Python::initialize();
384        Python::attach(|_py| {
385            let mut py_clock = PyClock::new_test();
386
387            py_clock.py_set_time(1_700_000_000_000_000_000).unwrap();
388
389            assert_eq!(py_clock.py_timestamp_ns(), 1_700_000_000_000_000_000);
390        });
391    }
392
393    #[rstest]
394    fn test_test_clock_py_set_timer() {
395        Python::initialize();
396        Python::attach(|_py| {
397            let mut py_clock = PyClock::new_test();
398            let callback = test_py_callback();
399            py_clock.py_register_default_handler(callback);
400            let interval = SignedDuration::from_secs(2);
401            py_clock
402                .py_set_timer("TIMER1", interval, None, None, None, None, None)
403                .expect("set_timer failed");
404        });
405    }
406
407    #[rstest]
408    fn test_test_clock_py_set_timer_rejects_unconvertible_datetime() {
409        Python::initialize();
410        Python::attach(|_py| {
411            let mut py_clock = PyClock::new_test();
412            let callback = test_py_callback();
413            py_clock.py_register_default_handler(callback);
414            let interval = SignedDuration::from_secs(2);
415            let pre_epoch = Timestamp::from_nanosecond(-1).unwrap();
416
417            let err = py_clock
418                .py_set_timer(
419                    "PRE_EPOCH_START",
420                    interval,
421                    Some(pre_epoch),
422                    None,
423                    None,
424                    None,
425                    None,
426                )
427                .expect_err("set_timer should reject a pre-epoch start time");
428            assert!(
429                err.to_string().contains("cannot be negative"),
430                "unexpected error: {err}"
431            );
432
433            let err = py_clock
434                .py_set_timer(
435                    "PRE_EPOCH_STOP",
436                    interval,
437                    None,
438                    Some(pre_epoch),
439                    None,
440                    None,
441                    None,
442                )
443                .expect_err("set_timer should reject a pre-epoch stop time");
444            assert!(
445                err.to_string().contains("cannot be negative"),
446                "unexpected error: {err}"
447            );
448
449            assert_eq!(py_clock.py_timer_count(), 0);
450        });
451    }
452
453    #[rstest]
454    fn test_test_clock_py_set_time_alert_ns() {
455        Python::initialize();
456        Python::attach(|_py| {
457            let mut py_clock = PyClock::new_test();
458            let callback = test_py_callback();
459            py_clock.py_register_default_handler(callback);
460            let ts_ns = (Timestamp::now() + SignedDuration::from_secs(1)).as_nanosecond();
461            let ts_ns = u64::try_from(ts_ns).unwrap();
462            py_clock
463                .py_set_time_alert_ns("ALERT_NS", ts_ns, None, None)
464                .expect("set_time_alert_ns failed");
465        });
466    }
467
468    #[rstest]
469    fn test_test_clock_py_set_timer_ns() {
470        Python::initialize();
471        Python::attach(|_py| {
472            let mut py_clock = PyClock::new_test();
473            let callback = test_py_callback();
474            py_clock.py_register_default_handler(callback);
475            py_clock
476                .py_set_timer_ns("TIMER_NS", 1_000_000, None, None, None, None, None)
477                .expect("set_timer_ns failed");
478        });
479    }
480
481    #[rstest]
482    fn test_test_clock_raw_set_timer_ns(mut test_clock: VirtualClock) {
483        Python::initialize();
484        Python::attach(|_py| {
485            let callback = test_callback();
486            test_clock.register_default_handler(callback);
487
488            let timer_name = "TEST_TIME1";
489            test_clock
490                .set_timer_ns(
491                    timer_name,
492                    DurationNanos::new(10),
493                    None,
494                    None,
495                    None,
496                    None,
497                    None,
498                )
499                .unwrap();
500
501            assert_eq!(test_clock.timer_names(), [timer_name]);
502            assert_eq!(test_clock.timer_count(), 1);
503        });
504    }
505
506    #[rstest]
507    fn test_test_clock_cancel_timer(mut test_clock: VirtualClock) {
508        Python::initialize();
509        Python::attach(|_py| {
510            let callback = test_callback();
511            test_clock.register_default_handler(callback);
512
513            let timer_name = "TEST_TIME1";
514            test_clock
515                .set_timer_ns(
516                    timer_name,
517                    DurationNanos::new(10),
518                    None,
519                    None,
520                    None,
521                    None,
522                    None,
523                )
524                .unwrap();
525            test_clock.cancel_timer(timer_name);
526
527            assert!(test_clock.timer_names().is_empty());
528            assert_eq!(test_clock.timer_count(), 0);
529        });
530    }
531
532    #[rstest]
533    fn test_test_clock_cancel_timers(mut test_clock: VirtualClock) {
534        Python::initialize();
535        Python::attach(|_py| {
536            let callback = test_callback();
537            test_clock.register_default_handler(callback);
538
539            let timer_name = "TEST_TIME1";
540            test_clock
541                .set_timer_ns(
542                    timer_name,
543                    DurationNanos::new(10),
544                    None,
545                    None,
546                    None,
547                    None,
548                    None,
549                )
550                .unwrap();
551            test_clock.cancel_timers();
552
553            assert!(test_clock.timer_names().is_empty());
554            assert_eq!(test_clock.timer_count(), 0);
555        });
556    }
557
558    #[rstest]
559    fn test_test_clock_advance_within_stop_time_py(mut test_clock: VirtualClock) {
560        Python::initialize();
561        Python::attach(|_py| {
562            let callback = test_callback();
563            test_clock.register_default_handler(callback);
564
565            let timer_name = "TEST_TIME1";
566            test_clock
567                .set_timer_ns(
568                    timer_name,
569                    DurationNanos::new(1),
570                    Some(UnixNanos::from(1)),
571                    Some(UnixNanos::from(3)),
572                    None,
573                    None,
574                    None,
575                )
576                .unwrap();
577            test_clock.advance_time(2.into(), true);
578
579            assert_eq!(test_clock.timer_names(), [timer_name]);
580            assert_eq!(test_clock.timer_count(), 1);
581        });
582    }
583
584    #[rstest]
585    fn test_test_clock_advance_time_to_stop_time_with_set_time_true(mut test_clock: VirtualClock) {
586        Python::initialize();
587        Python::attach(|_py| {
588            let callback = test_callback();
589            test_clock.register_default_handler(callback);
590
591            test_clock
592                .set_timer_ns(
593                    "TEST_TIME1",
594                    DurationNanos::new(2),
595                    None,
596                    Some(UnixNanos::from(3)),
597                    None,
598                    None,
599                    None,
600                )
601                .unwrap();
602            test_clock.advance_time(3.into(), true);
603
604            assert_eq!(test_clock.timer_names().len(), 1);
605            assert_eq!(test_clock.timer_count(), 1);
606            assert_eq!(test_clock.get_time_ns(), 3);
607        });
608    }
609
610    #[rstest]
611    fn test_test_clock_advance_time_to_stop_time_with_set_time_false(mut test_clock: VirtualClock) {
612        Python::initialize();
613        Python::attach(|_py| {
614            let callback = test_callback();
615            test_clock.register_default_handler(callback);
616
617            test_clock
618                .set_timer_ns(
619                    "TEST_TIME1",
620                    DurationNanos::new(2),
621                    None,
622                    Some(UnixNanos::from(3)),
623                    None,
624                    None,
625                    None,
626                )
627                .unwrap();
628            test_clock.advance_time(3.into(), false);
629
630            assert_eq!(test_clock.timer_names().len(), 1);
631            assert_eq!(test_clock.timer_count(), 1);
632            assert_eq!(test_clock.get_time_ns(), 0);
633        });
634    }
635
636    ////////////////////////////////////////////////////////////////////////////////
637    // LiveClock_Py
638    ////////////////////////////////////////////////////////////////////////////////
639
640    #[rstest]
641    fn test_live_clock_py_set_time_alert() {
642        ensure_sender();
643
644        Python::initialize();
645        Python::attach(|_py| {
646            let mut py_clock = PyClock::new_live();
647            let callback = test_py_callback();
648            py_clock.py_register_default_handler(callback);
649            let dt = Timestamp::now() + SignedDuration::from_secs(1);
650
651            py_clock
652                .py_set_time_alert("ALERT1", dt, None, None)
653                .expect("live set_time_alert failed");
654        });
655    }
656
657    #[rstest]
658    fn test_live_clock_py_set_time_returns_error() {
659        Python::initialize();
660        Python::attach(|_py| {
661            let mut py_clock = PyClock::new_live();
662
663            let result = py_clock.py_set_time(1_700_000_000_000_000_000);
664
665            assert_eq!(
666                result.unwrap_err().to_string(),
667                "ValueError: set_time is only supported by virtual clocks",
668            );
669        });
670    }
671
672    #[rstest]
673    fn test_live_clock_py_set_timer() {
674        ensure_sender();
675
676        Python::initialize();
677        Python::attach(|_py| {
678            let mut py_clock = PyClock::new_live();
679            let callback = test_py_callback();
680            py_clock.py_register_default_handler(callback);
681            let interval = SignedDuration::from_secs(3);
682
683            py_clock
684                .py_set_timer("TIMER1", interval, None, None, None, None, None)
685                .expect("live set_timer failed");
686        });
687    }
688
689    #[rstest]
690    fn test_live_clock_py_set_time_alert_ns() {
691        ensure_sender();
692
693        Python::initialize();
694        Python::attach(|_py| {
695            let mut py_clock = PyClock::new_live();
696            let callback = test_py_callback();
697            py_clock.py_register_default_handler(callback);
698            let dt_ns = (Timestamp::now() + SignedDuration::from_secs(1)).as_nanosecond();
699            let dt_ns = u64::try_from(dt_ns).unwrap();
700
701            py_clock
702                .py_set_time_alert_ns("ALERT_NS", dt_ns, None, None)
703                .expect("live set_time_alert_ns failed");
704        });
705    }
706
707    #[rstest]
708    fn test_live_clock_py_set_timer_ns() {
709        ensure_sender();
710
711        Python::initialize();
712        Python::attach(|_py| {
713            let mut py_clock = PyClock::new_live();
714            let callback = test_py_callback();
715            py_clock.py_register_default_handler(callback);
716            let interval_ns = 1_000_000_000_u64; // 1 second
717
718            py_clock
719                .py_set_timer_ns("TIMER_NS", interval_ns, None, None, None, None, None)
720                .expect("live set_timer_ns failed");
721        });
722    }
723}