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 chrono::{DateTime, Duration, Utc};
21use nautilus_core::{UnixNanos, 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.core.nautilus_pyo3.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 date and time as a timezone-aware `DateTime<UTC>`.
84    #[pyo3(name = "utc_now")]
85    fn py_utc_now(&self) -> DateTime<Utc> {
86        self.0.borrow().utc_now()
87    }
88
89    /// Returns the names of active timers in the clock.
90    #[pyo3(name = "timer_names")]
91    fn py_timer_names(&self) -> Vec<String> {
92        self.0
93            .borrow()
94            .timer_names()
95            .into_iter()
96            .map(String::from)
97            .collect()
98    }
99
100    /// Returns the count of active timers in the clock.
101    #[pyo3(name = "timer_count")]
102    fn py_timer_count(&self) -> usize {
103        self.0.borrow().timer_count()
104    }
105
106    #[pyo3(name = "register_default_handler")]
107    fn py_register_default_handler(&mut self, callback: Py<PyAny>) {
108        self.0
109            .borrow_mut()
110            .register_default_handler(TimeEventCallback::from(callback));
111    }
112
113    #[pyo3(name = "cancel_default_handler")]
114    fn py_cancel_default_handler(&mut self) {
115        self.0.borrow_mut().cancel_default_handler();
116    }
117
118    #[pyo3(name = "cancel_callbacks")]
119    fn py_cancel_callbacks(&mut self) {
120        self.0.borrow_mut().cancel_callbacks();
121    }
122
123    #[pyo3(
124        name = "set_time_alert",
125        signature = (name, alert_time, callback=None, allow_past=None)
126    )]
127    fn py_set_time_alert(
128        &mut self,
129        name: &str,
130        alert_time: DateTime<Utc>,
131        callback: Option<Py<PyAny>>,
132        allow_past: Option<bool>,
133    ) -> PyResult<()> {
134        self.0
135            .borrow_mut()
136            .set_time_alert(
137                name,
138                alert_time,
139                callback.map(TimeEventCallback::from),
140                allow_past,
141            )
142            .map_err(to_pyvalue_err)
143    }
144
145    #[pyo3(
146        name = "set_time_alert_ns",
147        signature = (name, alert_time_ns, callback=None, allow_past=None)
148    )]
149    fn py_set_time_alert_ns(
150        &mut self,
151        name: &str,
152        alert_time_ns: u64,
153        callback: Option<Py<PyAny>>,
154        allow_past: Option<bool>,
155    ) -> PyResult<()> {
156        self.0
157            .borrow_mut()
158            .set_time_alert_ns(
159                name,
160                alert_time_ns.into(),
161                callback.map(TimeEventCallback::from),
162                allow_past,
163            )
164            .map_err(to_pyvalue_err)
165    }
166
167    #[expect(clippy::too_many_arguments)]
168    #[pyo3(
169        name = "set_timer",
170        signature = (name, interval, start_time=None, stop_time=None, callback=None, allow_past=None, fire_immediately=None)
171    )]
172    fn py_set_timer(
173        &mut self,
174        name: &str,
175        interval: Duration,
176        start_time: Option<DateTime<Utc>>,
177        stop_time: Option<DateTime<Utc>>,
178        callback: Option<Py<PyAny>>,
179        allow_past: Option<bool>,
180        fire_immediately: Option<bool>,
181    ) -> PyResult<()> {
182        let interval_ns_i64 = interval
183            .num_nanoseconds()
184            .ok_or_else(|| to_pyvalue_err("Interval too large"))?;
185
186        if interval_ns_i64 <= 0 {
187            return Err(to_pyvalue_err("Interval must be positive"));
188        }
189        let interval_ns = interval_ns_i64 as u64;
190
191        self.0
192            .borrow_mut()
193            .set_timer_ns(
194                name,
195                interval_ns,
196                start_time.map(UnixNanos::from),
197                stop_time.map(UnixNanos::from),
198                callback.map(TimeEventCallback::from),
199                allow_past,
200                fire_immediately,
201            )
202            .map_err(to_pyvalue_err)
203    }
204
205    #[expect(clippy::too_many_arguments)]
206    #[pyo3(
207        name = "set_timer_ns",
208        signature = (name, interval_ns, start_time_ns=None, stop_time_ns=None, callback=None, allow_past=None, fire_immediately=None)
209    )]
210    fn py_set_timer_ns(
211        &mut self,
212        name: &str,
213        interval_ns: u64,
214        start_time_ns: Option<u64>,
215        stop_time_ns: Option<u64>,
216        callback: Option<Py<PyAny>>,
217        allow_past: Option<bool>,
218        fire_immediately: Option<bool>,
219    ) -> PyResult<()> {
220        self.0
221            .borrow_mut()
222            .set_timer_ns(
223                name,
224                interval_ns,
225                start_time_ns.map(UnixNanos::from),
226                stop_time_ns.map(UnixNanos::from),
227                callback.map(TimeEventCallback::from),
228                allow_past,
229                fire_immediately,
230            )
231            .map_err(to_pyvalue_err)
232    }
233
234    #[pyo3(name = "next_time_ns")]
235    fn py_next_time_ns(&self, name: &str) -> Option<u64> {
236        self.0.borrow().next_time_ns(name).map(|t| t.as_u64())
237    }
238
239    #[pyo3(name = "cancel_timer")]
240    fn py_cancel_timer(&mut self, name: &str) {
241        self.0.borrow_mut().cancel_timer(name);
242    }
243
244    #[pyo3(name = "cancel_timers")]
245    fn py_cancel_timers(&mut self) {
246        self.0.borrow_mut().cancel_timers();
247    }
248}
249
250impl PyClock {
251    /// Creates a `PyClock` directly from an `Rc<RefCell<dyn Clock>>`.
252    #[must_use]
253    pub fn from_rc(rc: Rc<RefCell<dyn Clock>>) -> Self {
254        Self(rc)
255    }
256
257    /// Gets the inner `Rc<RefCell<dyn Clock>>` for use in Rust code.
258    #[must_use]
259    pub fn clock_rc(&self) -> Rc<RefCell<dyn Clock>> {
260        Rc::clone(&self.0)
261    }
262
263    /// Creates a clock backed by [`TestClock`].
264    #[must_use]
265    pub fn new_test() -> Self {
266        Self(Rc::new(RefCell::new(TestClock::default())))
267    }
268
269    /// Creates a clock backed by [`LiveClock`].
270    #[must_use]
271    pub fn new_live() -> Self {
272        Self(Rc::new(RefCell::new(LiveClock::default())))
273    }
274
275    /// Provides access to the inner [`Clock`] trait object.
276    #[must_use]
277    pub fn inner(&self) -> std::cell::Ref<'_, dyn Clock> {
278        self.0.borrow()
279    }
280
281    /// Mutably accesses the underlying [`Clock`].
282    #[must_use]
283    pub fn inner_mut(&mut self) -> std::cell::RefMut<'_, dyn Clock> {
284        self.0.borrow_mut()
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use std::sync::Arc;
291
292    use chrono::{Duration, Utc};
293    use nautilus_core::{UnixNanos, python::IntoPyObjectNautilusExt};
294    use pyo3::{prelude::*, types::PyList};
295    use rstest::*;
296
297    use crate::{
298        clock::{Clock, TestClock},
299        python::clock::PyClock,
300        runner::{TimeEventSender, set_time_event_sender},
301        timer::{TimeEventCallback, TimeEventHandler},
302    };
303
304    fn ensure_sender() {
305        if crate::runner::try_get_time_event_sender().is_none() {
306            set_time_event_sender(Arc::new(DummySender));
307        }
308    }
309
310    // Dummy TimeEventSender for LiveClock tests
311    #[derive(Debug)]
312    struct DummySender;
313
314    impl TimeEventSender for DummySender {
315        fn send(&self, _handler: TimeEventHandler) {}
316    }
317
318    #[fixture]
319    pub fn test_clock() -> TestClock {
320        TestClock::new()
321    }
322
323    pub(super) fn test_callback() -> TimeEventCallback {
324        Python::initialize();
325        Python::attach(|py| {
326            let py_list = PyList::empty(py);
327            let py_append = Py::from(py_list.getattr("append").unwrap());
328            let py_append = py_append.into_py_any_unwrap(py);
329            TimeEventCallback::from(py_append)
330        })
331    }
332
333    pub(super) fn test_py_callback() -> Py<PyAny> {
334        Python::initialize();
335        Python::attach(|py| {
336            let py_list = PyList::empty(py);
337            let py_append = Py::from(py_list.getattr("append").unwrap());
338            py_append.into_py_any_unwrap(py)
339        })
340    }
341
342    ////////////////////////////////////////////////////////////////////////////////
343    // TestClock_Py
344    ////////////////////////////////////////////////////////////////////////////////
345
346    #[rstest]
347    fn test_test_clock_py_set_time_alert() {
348        Python::initialize();
349        Python::attach(|_py| {
350            let mut py_clock = PyClock::new_test();
351            let callback = test_py_callback();
352            py_clock.py_register_default_handler(callback);
353            let dt = Utc::now() + Duration::seconds(1);
354            py_clock
355                .py_set_time_alert("ALERT1", dt, None, None)
356                .expect("set_time_alert failed");
357        });
358    }
359
360    #[rstest]
361    fn test_test_clock_py_set_timer() {
362        Python::initialize();
363        Python::attach(|_py| {
364            let mut py_clock = PyClock::new_test();
365            let callback = test_py_callback();
366            py_clock.py_register_default_handler(callback);
367            let interval = Duration::seconds(2);
368            py_clock
369                .py_set_timer("TIMER1", interval, None, None, None, None, None)
370                .expect("set_timer failed");
371        });
372    }
373
374    #[rstest]
375    fn test_test_clock_py_set_time_alert_ns() {
376        Python::initialize();
377        Python::attach(|_py| {
378            let mut py_clock = PyClock::new_test();
379            let callback = test_py_callback();
380            py_clock.py_register_default_handler(callback);
381            let ts_ns = (Utc::now() + Duration::seconds(1))
382                .timestamp_nanos_opt()
383                .unwrap() as u64;
384            py_clock
385                .py_set_time_alert_ns("ALERT_NS", ts_ns, None, None)
386                .expect("set_time_alert_ns failed");
387        });
388    }
389
390    #[rstest]
391    fn test_test_clock_py_set_timer_ns() {
392        Python::initialize();
393        Python::attach(|_py| {
394            let mut py_clock = PyClock::new_test();
395            let callback = test_py_callback();
396            py_clock.py_register_default_handler(callback);
397            py_clock
398                .py_set_timer_ns("TIMER_NS", 1_000_000, None, None, None, None, None)
399                .expect("set_timer_ns failed");
400        });
401    }
402
403    #[rstest]
404    fn test_test_clock_raw_set_timer_ns(mut test_clock: TestClock) {
405        Python::initialize();
406        Python::attach(|_py| {
407            let callback = test_callback();
408            test_clock.register_default_handler(callback);
409
410            let timer_name = "TEST_TIME1";
411            test_clock
412                .set_timer_ns(timer_name, 10, None, None, None, None, None)
413                .unwrap();
414
415            assert_eq!(test_clock.timer_names(), [timer_name]);
416            assert_eq!(test_clock.timer_count(), 1);
417        });
418    }
419
420    #[rstest]
421    fn test_test_clock_cancel_timer(mut test_clock: TestClock) {
422        Python::initialize();
423        Python::attach(|_py| {
424            let callback = test_callback();
425            test_clock.register_default_handler(callback);
426
427            let timer_name = "TEST_TIME1";
428            test_clock
429                .set_timer_ns(timer_name, 10, None, None, None, None, None)
430                .unwrap();
431            test_clock.cancel_timer(timer_name);
432
433            assert!(test_clock.timer_names().is_empty());
434            assert_eq!(test_clock.timer_count(), 0);
435        });
436    }
437
438    #[rstest]
439    fn test_test_clock_cancel_timers(mut test_clock: TestClock) {
440        Python::initialize();
441        Python::attach(|_py| {
442            let callback = test_callback();
443            test_clock.register_default_handler(callback);
444
445            let timer_name = "TEST_TIME1";
446            test_clock
447                .set_timer_ns(timer_name, 10, None, None, None, None, None)
448                .unwrap();
449            test_clock.cancel_timers();
450
451            assert!(test_clock.timer_names().is_empty());
452            assert_eq!(test_clock.timer_count(), 0);
453        });
454    }
455
456    #[rstest]
457    fn test_test_clock_advance_within_stop_time_py(mut test_clock: TestClock) {
458        Python::initialize();
459        Python::attach(|_py| {
460            let callback = test_callback();
461            test_clock.register_default_handler(callback);
462
463            let timer_name = "TEST_TIME1";
464            test_clock
465                .set_timer_ns(
466                    timer_name,
467                    1,
468                    Some(UnixNanos::from(1)),
469                    Some(UnixNanos::from(3)),
470                    None,
471                    None,
472                    None,
473                )
474                .unwrap();
475            test_clock.advance_time(2.into(), true);
476
477            assert_eq!(test_clock.timer_names(), [timer_name]);
478            assert_eq!(test_clock.timer_count(), 1);
479        });
480    }
481
482    #[rstest]
483    fn test_test_clock_advance_time_to_stop_time_with_set_time_true(mut test_clock: TestClock) {
484        Python::initialize();
485        Python::attach(|_py| {
486            let callback = test_callback();
487            test_clock.register_default_handler(callback);
488
489            test_clock
490                .set_timer_ns(
491                    "TEST_TIME1",
492                    2,
493                    None,
494                    Some(UnixNanos::from(3)),
495                    None,
496                    None,
497                    None,
498                )
499                .unwrap();
500            test_clock.advance_time(3.into(), true);
501
502            assert_eq!(test_clock.timer_names().len(), 1);
503            assert_eq!(test_clock.timer_count(), 1);
504            assert_eq!(test_clock.get_time_ns(), 3);
505        });
506    }
507
508    #[rstest]
509    fn test_test_clock_advance_time_to_stop_time_with_set_time_false(mut test_clock: TestClock) {
510        Python::initialize();
511        Python::attach(|_py| {
512            let callback = test_callback();
513            test_clock.register_default_handler(callback);
514
515            test_clock
516                .set_timer_ns(
517                    "TEST_TIME1",
518                    2,
519                    None,
520                    Some(UnixNanos::from(3)),
521                    None,
522                    None,
523                    None,
524                )
525                .unwrap();
526            test_clock.advance_time(3.into(), false);
527
528            assert_eq!(test_clock.timer_names().len(), 1);
529            assert_eq!(test_clock.timer_count(), 1);
530            assert_eq!(test_clock.get_time_ns(), 0);
531        });
532    }
533
534    ////////////////////////////////////////////////////////////////////////////////
535    // LiveClock_Py
536    ////////////////////////////////////////////////////////////////////////////////
537
538    #[rstest]
539    fn test_live_clock_py_set_time_alert() {
540        ensure_sender();
541
542        Python::initialize();
543        Python::attach(|_py| {
544            let mut py_clock = PyClock::new_live();
545            let callback = test_py_callback();
546            py_clock.py_register_default_handler(callback);
547            let dt = Utc::now() + Duration::seconds(1);
548
549            py_clock
550                .py_set_time_alert("ALERT1", dt, None, None)
551                .expect("live set_time_alert failed");
552        });
553    }
554
555    #[rstest]
556    fn test_live_clock_py_set_timer() {
557        ensure_sender();
558
559        Python::initialize();
560        Python::attach(|_py| {
561            let mut py_clock = PyClock::new_live();
562            let callback = test_py_callback();
563            py_clock.py_register_default_handler(callback);
564            let interval = Duration::seconds(3);
565
566            py_clock
567                .py_set_timer("TIMER1", interval, None, None, None, None, None)
568                .expect("live set_timer failed");
569        });
570    }
571
572    #[rstest]
573    fn test_live_clock_py_set_time_alert_ns() {
574        ensure_sender();
575
576        Python::initialize();
577        Python::attach(|_py| {
578            let mut py_clock = PyClock::new_live();
579            let callback = test_py_callback();
580            py_clock.py_register_default_handler(callback);
581            let dt_ns = (Utc::now() + Duration::seconds(1))
582                .timestamp_nanos_opt()
583                .unwrap() as u64;
584
585            py_clock
586                .py_set_time_alert_ns("ALERT_NS", dt_ns, None, None)
587                .expect("live set_time_alert_ns failed");
588        });
589    }
590
591    #[rstest]
592    fn test_live_clock_py_set_timer_ns() {
593        ensure_sender();
594
595        Python::initialize();
596        Python::attach(|_py| {
597            let mut py_clock = PyClock::new_live();
598            let callback = test_py_callback();
599            py_clock.py_register_default_handler(callback);
600            let interval_ns = 1_000_000_000_u64; // 1 second
601
602            py_clock
603                .py_set_timer_ns("TIMER_NS", interval_ns, None, None, None, None, None)
604                .expect("live set_timer_ns failed");
605        });
606    }
607}