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::{UnixNanos, python::to_pyvalue_err};
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, python::IntoPyObjectNautilusExt};
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            let py_append = py_append.into_py_any_unwrap(py);
213            TimeEventCallback::from(py_append)
214        })
215    }
216
217    #[rstest]
218    fn test_set_timer_ns_py(mut test_clock: TestClock) {
219        pyo3::prepare_freethreaded_python();
220
221        Python::with_gil(|_py| {
222            let callback = test_callback();
223            test_clock.register_default_handler(callback);
224
225            let timer_name = "TEST_TIME1";
226            test_clock
227                .set_timer_ns(timer_name, 10, 0.into(), None, None)
228                .unwrap();
229
230            assert_eq!(test_clock.timer_names(), [timer_name]);
231            assert_eq!(test_clock.timer_count(), 1);
232        });
233    }
234
235    #[rstest]
236    fn test_cancel_timer(mut test_clock: TestClock) {
237        pyo3::prepare_freethreaded_python();
238
239        Python::with_gil(|_py| {
240            let callback = test_callback();
241            test_clock.register_default_handler(callback);
242
243            let timer_name = "TEST_TIME1";
244            test_clock
245                .set_timer_ns(timer_name, 10, 0.into(), None, None)
246                .unwrap();
247            test_clock.cancel_timer(timer_name);
248
249            assert!(test_clock.timer_names().is_empty());
250            assert_eq!(test_clock.timer_count(), 0);
251        });
252    }
253
254    #[rstest]
255    fn test_cancel_timers(mut test_clock: TestClock) {
256        pyo3::prepare_freethreaded_python();
257
258        Python::with_gil(|_py| {
259            let callback = test_callback();
260            test_clock.register_default_handler(callback);
261
262            let timer_name = "TEST_TIME1";
263            test_clock
264                .set_timer_ns(timer_name, 10, 0.into(), None, None)
265                .unwrap();
266            test_clock.cancel_timers();
267
268            assert!(test_clock.timer_names().is_empty());
269            assert_eq!(test_clock.timer_count(), 0);
270        });
271    }
272
273    #[rstest]
274    fn test_advance_within_stop_time_py(mut test_clock: TestClock) {
275        pyo3::prepare_freethreaded_python();
276
277        Python::with_gil(|_py| {
278            let callback = test_callback();
279            test_clock.register_default_handler(callback);
280
281            let timer_name = "TEST_TIME1";
282            test_clock
283                .set_timer_ns(timer_name, 1, 1.into(), Some(UnixNanos::from(3)), None)
284                .unwrap();
285            test_clock.advance_time(2.into(), true);
286
287            assert_eq!(test_clock.timer_names(), [timer_name]);
288            assert_eq!(test_clock.timer_count(), 1);
289        });
290    }
291
292    #[rstest]
293    fn test_advance_time_to_stop_time_with_set_time_true(mut test_clock: TestClock) {
294        pyo3::prepare_freethreaded_python();
295
296        Python::with_gil(|_py| {
297            let callback = test_callback();
298            test_clock.register_default_handler(callback);
299
300            test_clock
301                .set_timer_ns("TEST_TIME1", 2, 0.into(), Some(UnixNanos::from(3)), None)
302                .unwrap();
303            test_clock.advance_time(3.into(), true);
304
305            assert_eq!(test_clock.timer_names().len(), 1);
306            assert_eq!(test_clock.timer_count(), 1);
307            assert_eq!(test_clock.get_time_ns(), 3);
308        });
309    }
310
311    #[rstest]
312    fn test_advance_time_to_stop_time_with_set_time_false(mut test_clock: TestClock) {
313        pyo3::prepare_freethreaded_python();
314
315        Python::with_gil(|_py| {
316            let callback = test_callback();
317            test_clock.register_default_handler(callback);
318
319            test_clock
320                .set_timer_ns("TEST_TIME1", 2, 0.into(), Some(UnixNanos::from(3)), None)
321                .unwrap();
322            test_clock.advance_time(3.into(), false);
323
324            assert_eq!(test_clock.timer_names().len(), 1);
325            assert_eq!(test_clock.timer_count(), 1);
326            assert_eq!(test_clock.get_time_ns(), 0);
327        });
328    }
329}