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