nautilus_common/python/
clock.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.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 = "posei_trader.core.nautilus_pyo3.common",
36    name = "TestClock",
37    unsendable
38)]
39#[derive(Debug)]
40pub struct TestClock_Py(Box<TestClock>);
41
42#[pymethods]
43impl TestClock_Py {
44    #[new]
45    fn py_new() -> Self {
46        Self(Box::default())
47    }
48
49    fn advance_time(&mut self, to_time_ns: u64, set_time: bool) -> Vec<TimeEvent> {
50        self.0.advance_time(to_time_ns.into(), set_time)
51    }
52
53    fn match_handlers(&self, events: Vec<TimeEvent>) -> Vec<TimeEventHandler_Py> {
54        self.0
55            .match_handlers(events)
56            .into_iter()
57            .map(Into::into)
58            .collect()
59    }
60
61    fn register_default_handler(&mut self, callback: PyObject) {
62        self.0
63            .register_default_handler(TimeEventCallback::from(callback));
64    }
65
66    #[pyo3(signature = (name, alert_time_ns, callback=None, allow_past=None))]
67    fn set_time_alert_ns(
68        &mut self,
69        name: &str,
70        alert_time_ns: u64,
71        callback: Option<PyObject>,
72        allow_past: Option<bool>,
73    ) -> PyResult<()> {
74        self.0
75            .set_time_alert_ns(
76                name,
77                alert_time_ns.into(),
78                callback.map(TimeEventCallback::from),
79                allow_past,
80            )
81            .map_err(to_pyvalue_err)
82    }
83
84    #[allow(clippy::too_many_arguments)]
85    #[pyo3(signature = (name, interval_ns, start_time_ns, stop_time_ns=None, callback=None, allow_past=None, fire_immediately=None))]
86    fn set_timer_ns(
87        &mut self,
88        name: &str,
89        interval_ns: u64,
90        start_time_ns: Option<u64>,
91        stop_time_ns: Option<u64>,
92        callback: Option<PyObject>,
93        allow_past: Option<bool>,
94        fire_immediately: Option<bool>,
95    ) -> PyResult<()> {
96        self.0
97            .set_timer_ns(
98                name,
99                interval_ns,
100                start_time_ns.map(UnixNanos::from),
101                stop_time_ns.map(UnixNanos::from),
102                callback.map(TimeEventCallback::from),
103                allow_past,
104                fire_immediately,
105            )
106            .map_err(to_pyvalue_err)
107    }
108
109    fn next_time_ns(&self, name: &str) -> Option<u64> {
110        self.0.next_time_ns(name).map(|n| n.as_u64())
111    }
112
113    fn cancel_timer(&mut self, name: &str) {
114        self.0.cancel_timer(name);
115    }
116
117    fn cancel_timers(&mut self) {
118        self.0.cancel_timers();
119    }
120}
121
122/// PyO3 compatible interface for an underlying [`LiveClock`].
123///
124/// This struct wraps `LiveClock` in a way that makes it possible to create
125/// Python bindings for it.
126///
127/// It implements the `Deref` trait, allowing instances of `LiveClock_Py` to be
128/// dereferenced to `LiveClock`, providing access to `LiveClock`'s methods without
129/// having to manually access the underlying `LiveClock` instance.
130#[allow(non_camel_case_types)]
131#[pyo3::pyclass(
132    module = "posei_trader.core.nautilus_pyo3.common",
133    name = "LiveClock",
134    unsendable
135)]
136#[derive(Debug)]
137pub struct LiveClock_Py(Box<LiveClock>);
138
139#[pymethods]
140impl LiveClock_Py {
141    #[new]
142    fn py_new() -> Self {
143        Self(Box::default())
144    }
145
146    fn register_default_handler(&mut self, callback: PyObject) {
147        self.0
148            .register_default_handler(TimeEventCallback::from(callback));
149    }
150
151    #[pyo3(signature = (name, alert_time_ns, callback=None, allow_past=None))]
152    fn set_time_alert_ns(
153        &mut self,
154        name: &str,
155        alert_time_ns: u64,
156        callback: Option<PyObject>,
157        allow_past: Option<bool>,
158    ) -> PyResult<()> {
159        self.0
160            .set_time_alert_ns(
161                name,
162                alert_time_ns.into(),
163                callback.map(TimeEventCallback::from),
164                allow_past,
165            )
166            .map_err(to_pyvalue_err)
167    }
168
169    #[allow(clippy::too_many_arguments)]
170    #[pyo3(signature = (name, interval_ns, start_time_ns, stop_time_ns=None, callback=None, allow_past=None, fire_immediately=None))]
171    fn set_timer_ns(
172        &mut self,
173        name: &str,
174        interval_ns: u64,
175        start_time_ns: Option<u64>,
176        stop_time_ns: Option<u64>,
177        callback: Option<PyObject>,
178        allow_past: Option<bool>,
179        fire_immediately: Option<bool>,
180    ) -> PyResult<()> {
181        self.0
182            .set_timer_ns(
183                name,
184                interval_ns,
185                start_time_ns.map(UnixNanos::from),
186                stop_time_ns.map(UnixNanos::from),
187                callback.map(TimeEventCallback::from),
188                allow_past,
189                fire_immediately,
190            )
191            .map_err(to_pyvalue_err)
192    }
193
194    fn next_time_ns(&self, name: &str) -> Option<u64> {
195        self.0.next_time_ns(name).map(|t| t.as_u64())
196    }
197
198    fn cancel_timer(&mut self, name: &str) {
199        self.0.cancel_timer(name);
200    }
201
202    fn cancel_timers(&mut self) {
203        self.0.cancel_timers();
204    }
205}
206
207////////////////////////////////////////////////////////////////////////////////
208// Tests
209////////////////////////////////////////////////////////////////////////////////
210#[cfg(test)]
211mod tests {
212    use nautilus_core::{UnixNanos, python::IntoPyObjectPoseiExt};
213    use pyo3::{prelude::*, types::PyList};
214    use rstest::*;
215
216    use crate::{
217        clock::{Clock, TestClock},
218        timer::TimeEventCallback,
219    };
220
221    #[fixture]
222    pub fn test_clock() -> TestClock {
223        TestClock::new()
224    }
225
226    pub fn test_callback() -> TimeEventCallback {
227        Python::with_gil(|py| {
228            let py_list = PyList::empty(py);
229            let py_append = Py::from(py_list.getattr("append").unwrap());
230            let py_append = py_append.into_py_any_unwrap(py);
231            TimeEventCallback::from(py_append)
232        })
233    }
234
235    #[rstest]
236    fn test_set_timer_ns_py(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, None, None, None, None, None)
246                .unwrap();
247
248            assert_eq!(test_clock.timer_names(), [timer_name]);
249            assert_eq!(test_clock.timer_count(), 1);
250        });
251    }
252
253    #[rstest]
254    fn test_cancel_timer(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, None, None, None, None, None)
264                .unwrap();
265            test_clock.cancel_timer(timer_name);
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_cancel_timers(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, 10, None, None, None, None, None)
283                .unwrap();
284            test_clock.cancel_timers();
285
286            assert!(test_clock.timer_names().is_empty());
287            assert_eq!(test_clock.timer_count(), 0);
288        });
289    }
290
291    #[rstest]
292    fn test_advance_within_stop_time_py(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            let timer_name = "TEST_TIME1";
300            test_clock
301                .set_timer_ns(
302                    timer_name,
303                    1,
304                    Some(UnixNanos::from(1)),
305                    Some(UnixNanos::from(3)),
306                    None,
307                    None,
308                    None,
309                )
310                .unwrap();
311            test_clock.advance_time(2.into(), true);
312
313            assert_eq!(test_clock.timer_names(), [timer_name]);
314            assert_eq!(test_clock.timer_count(), 1);
315        });
316    }
317
318    #[rstest]
319    fn test_advance_time_to_stop_time_with_set_time_true(mut test_clock: TestClock) {
320        pyo3::prepare_freethreaded_python();
321
322        Python::with_gil(|_py| {
323            let callback = test_callback();
324            test_clock.register_default_handler(callback);
325
326            test_clock
327                .set_timer_ns(
328                    "TEST_TIME1",
329                    2,
330                    None,
331                    Some(UnixNanos::from(3)),
332                    None,
333                    None,
334                    None,
335                )
336                .unwrap();
337            test_clock.advance_time(3.into(), true);
338
339            assert_eq!(test_clock.timer_names().len(), 1);
340            assert_eq!(test_clock.timer_count(), 1);
341            assert_eq!(test_clock.get_time_ns(), 3);
342        });
343    }
344
345    #[rstest]
346    fn test_advance_time_to_stop_time_with_set_time_false(mut test_clock: TestClock) {
347        pyo3::prepare_freethreaded_python();
348
349        Python::with_gil(|_py| {
350            let callback = test_callback();
351            test_clock.register_default_handler(callback);
352
353            test_clock
354                .set_timer_ns(
355                    "TEST_TIME1",
356                    2,
357                    None,
358                    Some(UnixNanos::from(3)),
359                    None,
360                    None,
361                    None,
362                )
363                .unwrap();
364            test_clock.advance_time(3.into(), false);
365
366            assert_eq!(test_clock.timer_names().len(), 1);
367            assert_eq!(test_clock.timer_count(), 1);
368            assert_eq!(test_clock.get_time_ns(), 0);
369        });
370    }
371}