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