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