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