nautilus_common/python/
clock.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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 nautilus_core::{python::to_pyvalue_err, UnixNanos};
17use pyo3::prelude::*;
18
19use super::timer::TimeEventHandler_Py;
20use crate::{
21    clock::{Clock, LiveClock, TestClock},
22    timer::{TimeEvent, TimeEventCallback},
23};
24
25/// PyO3 compatible interface for an underlying [`TestClock`].
26///
27/// This struct wraps `TestClock` in a way that makes it possible to create
28/// Python bindings for it.
29///
30/// It implements the `Deref` trait, allowing instances of `TestClock_API` to be
31/// dereferenced to `TestClock`, providing access to `TestClock`'s methods without
32/// having to manually access the underlying `TestClock` instance.
33#[allow(non_camel_case_types)]
34#[pyo3::pyclass(
35    module = "nautilus_trader.core.nautilus_pyo3.common",
36    name = "TestClock"
37)]
38pub struct TestClock_Py(Box<TestClock>);
39
40#[pymethods]
41impl TestClock_Py {
42    #[new]
43    fn py_new() -> Self {
44        Self(Box::new(TestClock::new()))
45    }
46
47    fn advance_time(&mut self, to_time_ns: u64, set_time: bool) -> Vec<TimeEvent> {
48        self.0.advance_time(to_time_ns.into(), set_time)
49    }
50
51    fn match_handlers(&self, events: Vec<TimeEvent>) -> Vec<TimeEventHandler_Py> {
52        self.0
53            .match_handlers(events)
54            .into_iter()
55            .map(Into::into)
56            .collect()
57    }
58
59    fn register_default_handler(&mut self, callback: PyObject) {
60        self.0
61            .register_default_handler(TimeEventCallback::from(callback));
62    }
63
64    #[pyo3(signature = (name, alert_time_ns, callback=None))]
65    fn set_time_alert_ns(
66        &mut self,
67        name: &str,
68        alert_time_ns: u64,
69        callback: Option<PyObject>,
70    ) -> PyResult<()> {
71        self.0
72            .set_time_alert_ns(
73                name,
74                alert_time_ns.into(),
75                callback.map(TimeEventCallback::from),
76            )
77            .map_err(to_pyvalue_err)
78    }
79
80    #[pyo3(signature = (name, interval_ns, start_time_ns, stop_time_ns=None, callback=None))]
81    fn set_timer_ns(
82        &mut self,
83        name: &str,
84        interval_ns: u64,
85        start_time_ns: u64,
86        stop_time_ns: Option<u64>,
87        callback: Option<PyObject>,
88    ) -> PyResult<()> {
89        self.0
90            .set_timer_ns(
91                name,
92                interval_ns,
93                start_time_ns.into(),
94                stop_time_ns.map(UnixNanos::from),
95                callback.map(TimeEventCallback::from),
96            )
97            .map_err(to_pyvalue_err)
98    }
99
100    fn next_time_ns(&self, name: &str) -> u64 {
101        *self.0.next_time_ns(name)
102    }
103
104    fn cancel_timer(&mut self, name: &str) {
105        self.0.cancel_timer(name);
106    }
107
108    fn cancel_timers(&mut self) {
109        self.0.cancel_timers();
110    }
111}
112
113/// PyO3 compatible interface for an underlying [`LiveClock`].
114///
115/// This struct wraps `LiveClock` in a way that makes it possible to create
116/// Python bindings for it.
117///
118/// It implements the `Deref` trait, allowing instances of `LiveClock_Py` to be
119/// dereferenced to `LiveClock`, providing access to `LiveClock`'s methods without
120/// having to manually access the underlying `LiveClock` instance.
121#[allow(non_camel_case_types)]
122#[pyo3::pyclass(
123    module = "nautilus_trader.core.nautilus_pyo3.common",
124    name = "LiveClock"
125)]
126pub struct LiveClock_Py(Box<LiveClock>);
127
128#[pymethods]
129impl LiveClock_Py {
130    #[new]
131    fn py_new() -> Self {
132        Self(Box::new(LiveClock::new()))
133    }
134
135    fn register_default_handler(&mut self, callback: PyObject) {
136        self.0
137            .register_default_handler(TimeEventCallback::from(callback));
138    }
139
140    #[pyo3(signature = (name, alert_time_ns, callback=None))]
141    fn set_time_alert_ns(
142        &mut self,
143        name: &str,
144        alert_time_ns: u64,
145        callback: Option<PyObject>,
146    ) -> PyResult<()> {
147        self.0
148            .set_time_alert_ns(
149                name,
150                alert_time_ns.into(),
151                callback.map(TimeEventCallback::from),
152            )
153            .map_err(to_pyvalue_err)
154    }
155
156    #[pyo3(signature = (name, interval_ns, start_time_ns, stop_time_ns=None, callback=None))]
157    fn set_timer_ns(
158        &mut self,
159        name: &str,
160        interval_ns: u64,
161        start_time_ns: u64,
162        stop_time_ns: Option<u64>,
163        callback: Option<PyObject>,
164    ) -> PyResult<()> {
165        self.0
166            .set_timer_ns(
167                name,
168                interval_ns,
169                start_time_ns.into(),
170                stop_time_ns.map(UnixNanos::from),
171                callback.map(TimeEventCallback::from),
172            )
173            .map_err(to_pyvalue_err)
174    }
175
176    fn next_time_ns(&self, name: &str) -> u64 {
177        *self.0.next_time_ns(name)
178    }
179
180    fn cancel_timer(&mut self, name: &str) {
181        self.0.cancel_timer(name);
182    }
183
184    fn cancel_timers(&mut self) {
185        self.0.cancel_timers();
186    }
187}
188
189////////////////////////////////////////////////////////////////////////////////
190// Tests
191////////////////////////////////////////////////////////////////////////////////
192#[cfg(test)]
193mod tests {
194    use nautilus_core::UnixNanos;
195    use pyo3::{prelude::*, types::PyList};
196    use rstest::*;
197
198    use crate::{
199        clock::{Clock, TestClock},
200        timer::TimeEventCallback,
201    };
202
203    #[fixture]
204    pub fn test_clock() -> TestClock {
205        TestClock::new()
206    }
207
208    pub fn test_callback() -> TimeEventCallback {
209        Python::with_gil(|py| {
210            let py_list = PyList::empty(py);
211            let py_append = Py::from(py_list.getattr("append").unwrap());
212            TimeEventCallback::from(py_append.into_py(py))
213        })
214    }
215
216    #[rstest]
217    fn test_set_timer_ns_py(mut test_clock: TestClock) {
218        pyo3::prepare_freethreaded_python();
219
220        Python::with_gil(|_py| {
221            let callback = test_callback();
222            test_clock.register_default_handler(callback);
223
224            let timer_name = "TEST_TIME1";
225            test_clock
226                .set_timer_ns(timer_name, 10, 0.into(), None, None)
227                .unwrap();
228
229            assert_eq!(test_clock.timer_names(), [timer_name]);
230            assert_eq!(test_clock.timer_count(), 1);
231        });
232    }
233
234    #[rstest]
235    fn test_cancel_timer(mut test_clock: TestClock) {
236        pyo3::prepare_freethreaded_python();
237
238        Python::with_gil(|_py| {
239            let callback = test_callback();
240            test_clock.register_default_handler(callback);
241
242            let timer_name = "TEST_TIME1";
243            test_clock
244                .set_timer_ns(timer_name, 10, 0.into(), None, None)
245                .unwrap();
246            test_clock.cancel_timer(timer_name);
247
248            assert!(test_clock.timer_names().is_empty());
249            assert_eq!(test_clock.timer_count(), 0);
250        });
251    }
252
253    #[rstest]
254    fn test_cancel_timers(mut test_clock: TestClock) {
255        pyo3::prepare_freethreaded_python();
256
257        Python::with_gil(|_py| {
258            let callback = test_callback();
259            test_clock.register_default_handler(callback);
260
261            let timer_name = "TEST_TIME1";
262            test_clock
263                .set_timer_ns(timer_name, 10, 0.into(), None, None)
264                .unwrap();
265            test_clock.cancel_timers();
266
267            assert!(test_clock.timer_names().is_empty());
268            assert_eq!(test_clock.timer_count(), 0);
269        });
270    }
271
272    #[rstest]
273    fn test_advance_within_stop_time_py(mut test_clock: TestClock) {
274        pyo3::prepare_freethreaded_python();
275
276        Python::with_gil(|_py| {
277            let callback = test_callback();
278            test_clock.register_default_handler(callback);
279
280            let timer_name = "TEST_TIME1";
281            test_clock
282                .set_timer_ns(timer_name, 1, 1.into(), Some(UnixNanos::from(3)), None)
283                .unwrap();
284            test_clock.advance_time(2.into(), true);
285
286            assert_eq!(test_clock.timer_names(), [timer_name]);
287            assert_eq!(test_clock.timer_count(), 1);
288        });
289    }
290
291    #[rstest]
292    fn test_advance_time_to_stop_time_with_set_time_true(mut test_clock: TestClock) {
293        pyo3::prepare_freethreaded_python();
294
295        Python::with_gil(|_py| {
296            let callback = test_callback();
297            test_clock.register_default_handler(callback);
298
299            test_clock
300                .set_timer_ns("TEST_TIME1", 2, 0.into(), Some(UnixNanos::from(3)), None)
301                .unwrap();
302            test_clock.advance_time(3.into(), true);
303
304            assert_eq!(test_clock.timer_names().len(), 1);
305            assert_eq!(test_clock.timer_count(), 1);
306            assert_eq!(test_clock.get_time_ns(), 3);
307        });
308    }
309
310    #[rstest]
311    fn test_advance_time_to_stop_time_with_set_time_false(mut test_clock: TestClock) {
312        pyo3::prepare_freethreaded_python();
313
314        Python::with_gil(|_py| {
315            let callback = test_callback();
316            test_clock.register_default_handler(callback);
317
318            test_clock
319                .set_timer_ns("TEST_TIME1", 2, 0.into(), Some(UnixNanos::from(3)), None)
320                .unwrap();
321            test_clock.advance_time(3.into(), false);
322
323            assert_eq!(test_clock.timer_names().len(), 1);
324            assert_eq!(test_clock.timer_count(), 1);
325            assert_eq!(test_clock.get_time_ns(), 0);
326        });
327    }
328}