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