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