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}