nautilus_core/python/datetime.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
16//! Date/time utility wrappers exposed to Python.
17
18use pyo3::prelude::*;
19use pyo3_stub_gen::derive::gen_stub_pyfunction;
20
21use super::to_pyvalue_err;
22use crate::{
23 UnixNanos,
24 datetime::{
25 is_within_last_24_hours, last_weekday_nanos, micros_to_nanos, millis_to_nanos,
26 nanos_to_micros, nanos_to_millis, nanos_to_secs, secs_to_millis, secs_to_nanos,
27 unix_nanos_to_iso8601, unix_nanos_to_iso8601_millis,
28 },
29};
30
31/// Return round nanoseconds (ns) converted from the given seconds.
32///
33/// Parameters
34/// ----------
35/// secs : float
36/// The seconds to convert.
37///
38/// Returns
39/// -------
40/// int
41#[gen_stub_pyfunction(module = "nautilus_trader.core")]
42#[pyfunction(name = "secs_to_nanos")]
43pub fn py_secs_to_nanos(secs: f64) -> PyResult<u64> {
44 secs_to_nanos(secs).map_err(to_pyvalue_err)
45}
46
47/// Return round milliseconds (ms) converted from the given seconds.
48///
49/// Parameters
50/// ----------
51/// secs : float
52/// The seconds to convert.
53///
54/// Returns
55/// -------
56/// int
57#[gen_stub_pyfunction(module = "nautilus_trader.core")]
58#[pyfunction(name = "secs_to_millis")]
59pub fn py_secs_to_millis(secs: f64) -> PyResult<u64> {
60 secs_to_millis(secs).map_err(to_pyvalue_err)
61}
62
63/// Return round nanoseconds (ns) converted from the given milliseconds (ms).
64///
65/// Parameters
66/// ----------
67/// millis : float
68/// The milliseconds to convert.
69///
70/// Returns
71/// -------
72/// int
73#[gen_stub_pyfunction(module = "nautilus_trader.core")]
74#[pyfunction(name = "millis_to_nanos")]
75pub fn py_millis_to_nanos(millis: f64) -> PyResult<u64> {
76 millis_to_nanos(millis).map_err(to_pyvalue_err)
77}
78
79/// Return round nanoseconds (ns) converted from the given microseconds (μs).
80///
81/// Parameters
82/// ----------
83/// micros : float
84/// The microseconds to convert.
85///
86/// Returns
87/// -------
88/// int
89#[gen_stub_pyfunction(module = "nautilus_trader.core")]
90#[pyfunction(name = "micros_to_nanos")]
91pub fn py_micros_to_nanos(micros: f64) -> PyResult<u64> {
92 micros_to_nanos(micros).map_err(to_pyvalue_err)
93}
94
95/// Return seconds converted from the given nanoseconds (ns).
96///
97/// Parameters
98/// ----------
99/// nanos : int
100/// The nanoseconds to convert.
101///
102/// Returns
103/// -------
104/// float
105#[must_use]
106#[gen_stub_pyfunction(module = "nautilus_trader.core")]
107#[pyfunction(name = "nanos_to_secs")]
108pub fn py_nanos_to_secs(nanos: u64) -> f64 {
109 nanos_to_secs(nanos)
110}
111
112/// Return round milliseconds (ms) converted from the given nanoseconds (ns).
113///
114/// Parameters
115/// ----------
116/// nanos : int
117/// The nanoseconds to convert.
118///
119/// Returns
120/// -------
121/// int
122#[must_use]
123#[gen_stub_pyfunction(module = "nautilus_trader.core")]
124#[pyfunction(name = "nanos_to_millis")]
125pub const fn py_nanos_to_millis(nanos: u64) -> u64 {
126 nanos_to_millis(nanos)
127}
128
129/// Return round microseconds (μs) converted from the given nanoseconds (ns).
130///
131/// Parameters
132/// ----------
133/// nanos : int
134/// The nanoseconds to convert.
135///
136/// Returns
137/// -------
138/// int
139#[must_use]
140#[gen_stub_pyfunction(module = "nautilus_trader.core")]
141#[pyfunction(name = "nanos_to_micros")]
142pub const fn py_nanos_to_micros(nanos: u64) -> u64 {
143 nanos_to_micros(nanos)
144}
145
146/// Return UNIX nanoseconds as an ISO 8601 (RFC 3339) format string.
147///
148/// Parameters
149/// ----------
150/// timestamp_ns : int
151/// The UNIX timestamp (nanoseconds).
152/// nanos_precision : bool, default True
153/// If True, use nanosecond precision. If False, use millisecond precision.
154///
155/// Returns
156/// -------
157/// str
158///
159/// Raises
160/// ------
161/// ValueError
162/// If `timestamp_ns` is invalid.
163#[gen_stub_pyfunction(module = "nautilus_trader.core")]
164#[pyfunction(
165 name = "unix_nanos_to_iso8601",
166 signature = (timestamp_ns, nanos_precision=Some(true))
167)]
168pub fn py_unix_nanos_to_iso8601(
169 timestamp_ns: u64,
170 nanos_precision: Option<bool>,
171) -> PyResult<String> {
172 if timestamp_ns > i64::MAX as u64 {
173 return Err(to_pyvalue_err(
174 "timestamp_ns is out of range for conversion",
175 ));
176 }
177
178 let unix_nanos = UnixNanos::from(timestamp_ns);
179 let formatted = if nanos_precision.unwrap_or(true) {
180 unix_nanos_to_iso8601(unix_nanos)
181 } else {
182 unix_nanos_to_iso8601_millis(unix_nanos)
183 };
184
185 Ok(formatted)
186}
187
188/// Return UNIX nanoseconds at midnight (UTC) of the last weekday (Mon-Fri).
189///
190/// Parameters
191/// ----------
192/// year : int
193/// The year from the datum date.
194/// month : int
195/// The month from the datum date.
196/// day : int
197/// The day from the datum date.
198///
199/// Returns
200/// -------
201/// int
202///
203/// Raises
204/// ------
205/// `ValueError`
206/// If given an invalid date.
207///
208/// # Errors
209///
210/// Returns a `PyErr` if the provided date is invalid.
211#[gen_stub_pyfunction(module = "nautilus_trader.core")]
212#[pyfunction(name = "last_weekday_nanos")]
213pub fn py_last_weekday_nanos(year: i32, month: u32, day: u32) -> PyResult<u64> {
214 Ok(last_weekday_nanos(year, month, day)
215 .map_err(to_pyvalue_err)?
216 .as_u64())
217}
218
219/// Return whether the given UNIX nanoseconds timestamp is within the last 24 hours.
220///
221/// Parameters
222/// ----------
223/// timestamp_ns : int
224/// The UNIX nanoseconds timestamp datum.
225///
226/// Returns
227/// -------
228/// bool
229///
230/// Raises
231/// ------
232/// ValueError
233/// If `timestamp` is invalid.
234///
235/// # Errors
236///
237/// Returns a `PyErr` if the provided timestamp is invalid.
238#[gen_stub_pyfunction(module = "nautilus_trader.core")]
239#[pyfunction(name = "is_within_last_24_hours")]
240pub fn py_is_within_last_24_hours(timestamp_ns: u64) -> PyResult<bool> {
241 is_within_last_24_hours(UnixNanos::from(timestamp_ns)).map_err(to_pyvalue_err)
242}
243
244#[cfg(test)]
245mod tests {
246 use rstest::rstest;
247
248 use super::*;
249
250 #[rstest]
251 fn test_py_unix_nanos_to_iso8601_errors_on_out_of_range_timestamp() {
252 let result = py_unix_nanos_to_iso8601((i64::MAX as u64) + 1, Some(true));
253 assert!(result.is_err());
254 }
255
256 #[rstest]
257 fn test_py_unix_nanos_to_iso8601_formats_valid_timestamp() {
258 let output = py_unix_nanos_to_iso8601(0, Some(false)).unwrap();
259 assert_eq!(output, "1970-01-01T00:00:00.000Z");
260 }
261}