Skip to main content

nautilus_trading/
sessions.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//! Provides utilities for determining Forex session times.
17//! Includes functions to convert UTC times to session local times
18//! and retrieve the next or previous session start/end.
19//!
20//! All FX sessions run Monday to Friday local time:
21//!
22//! - Sydney Session    0700-1600 (Australia / Sydney)
23//! - Tokyo Session     0900-1800 (Asia / Tokyo)
24//! - London Session    0800-1600 (Europe / London)
25//! - New York Session  0800-1700 (America / New York)
26
27use chrono::{DateTime, Datelike, Duration, NaiveTime, TimeZone, Timelike, Utc};
28use chrono_tz::{America::New_York, Asia::Tokyo, Australia::Sydney, Europe::London, Tz};
29use strum::{Display, EnumIter, EnumString, FromRepr};
30
31/// Represents a major Forex market session based on trading hours.
32#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, FromRepr, EnumIter, EnumString, Display)]
33#[strum(ascii_case_insensitive)]
34#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
35#[cfg_attr(
36    feature = "python",
37    pyo3::pyclass(
38        eq,
39        eq_int,
40        module = "nautilus_trader.core.nautilus_pyo3.common.enums",
41        from_py_object,
42        rename_all = "SCREAMING_SNAKE_CASE"
43    )
44)]
45#[cfg_attr(
46    feature = "python",
47    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.trading")
48)]
49pub enum ForexSession {
50    Sydney,
51    Tokyo,
52    London,
53    NewYork,
54}
55
56impl ForexSession {
57    /// Returns the timezone associated with the session.
58    const fn timezone(&self) -> Tz {
59        match self {
60            Self::Sydney => Sydney,
61            Self::Tokyo => Tokyo,
62            Self::London => London,
63            Self::NewYork => New_York,
64        }
65    }
66
67    /// Returns the start and end times for the session in local time.
68    const fn session_times(&self) -> (NaiveTime, NaiveTime) {
69        match self {
70            Self::Sydney => (
71                NaiveTime::from_hms_opt(7, 0, 0).unwrap(),
72                NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
73            ),
74            Self::Tokyo => (
75                NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
76                NaiveTime::from_hms_opt(18, 0, 0).unwrap(),
77            ),
78            Self::London => (
79                NaiveTime::from_hms_opt(8, 0, 0).unwrap(),
80                NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
81            ),
82            Self::NewYork => (
83                NaiveTime::from_hms_opt(8, 0, 0).unwrap(),
84                NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
85            ),
86        }
87    }
88}
89
90/// Converts a UTC timestamp to the local time for the given Forex session.
91#[must_use]
92pub fn fx_local_from_utc(session: ForexSession, time_now: DateTime<Utc>) -> DateTime<Tz> {
93    session.timezone().from_utc_datetime(&time_now.naive_utc())
94}
95
96/// Returns the next session start time in UTC.
97#[must_use]
98pub fn fx_next_start(session: ForexSession, time_now: DateTime<Utc>) -> DateTime<Utc> {
99    let timezone = session.timezone();
100    let local_now = fx_local_from_utc(session, time_now);
101    let (start_time, _) = session.session_times();
102
103    let mut next_start = timezone
104        .with_ymd_and_hms(
105            local_now.year(),
106            local_now.month(),
107            local_now.day(),
108            start_time.hour(),
109            start_time.minute(),
110            0,
111        )
112        .unwrap();
113
114    if local_now > next_start {
115        next_start += Duration::days(1);
116    }
117
118    if next_start.weekday().number_from_monday() > 5 {
119        next_start += Duration::days(8 - i64::from(next_start.weekday().number_from_monday()));
120    }
121
122    next_start.with_timezone(&Utc)
123}
124
125/// Returns the previous session start time in UTC.
126#[must_use]
127pub fn fx_prev_start(session: ForexSession, time_now: DateTime<Utc>) -> DateTime<Utc> {
128    let timezone = session.timezone();
129    let local_now = fx_local_from_utc(session, time_now);
130    let (start_time, _) = session.session_times();
131
132    let mut prev_start = timezone
133        .with_ymd_and_hms(
134            local_now.year(),
135            local_now.month(),
136            local_now.day(),
137            start_time.hour(),
138            start_time.minute(),
139            0,
140        )
141        .unwrap();
142
143    if local_now < prev_start {
144        prev_start -= Duration::days(1);
145    }
146
147    if prev_start.weekday().number_from_monday() > 5 {
148        prev_start -= Duration::days(i64::from(prev_start.weekday().number_from_monday()) - 5);
149    }
150
151    prev_start.with_timezone(&Utc)
152}
153
154/// Returns the next session end time in UTC.
155#[must_use]
156pub fn fx_next_end(session: ForexSession, time_now: DateTime<Utc>) -> DateTime<Utc> {
157    let timezone = session.timezone();
158    let local_now = fx_local_from_utc(session, time_now);
159    let (_, end_time) = session.session_times();
160
161    let mut next_end = timezone
162        .with_ymd_and_hms(
163            local_now.year(),
164            local_now.month(),
165            local_now.day(),
166            end_time.hour(),
167            end_time.minute(),
168            0,
169        )
170        .unwrap();
171
172    if local_now > next_end {
173        next_end += Duration::days(1);
174    }
175
176    if next_end.weekday().number_from_monday() > 5 {
177        next_end += Duration::days(8 - i64::from(next_end.weekday().number_from_monday()));
178    }
179
180    next_end.with_timezone(&Utc)
181}
182
183/// Returns the previous session end time in UTC.
184#[must_use]
185pub fn fx_prev_end(session: ForexSession, time_now: DateTime<Utc>) -> DateTime<Utc> {
186    let timezone = session.timezone();
187    let local_now = fx_local_from_utc(session, time_now);
188    let (_, end_time) = session.session_times();
189
190    let mut prev_end = timezone
191        .with_ymd_and_hms(
192            local_now.year(),
193            local_now.month(),
194            local_now.day(),
195            end_time.hour(),
196            end_time.minute(),
197            0,
198        )
199        .unwrap();
200
201    if local_now < prev_end {
202        prev_end -= Duration::days(1);
203    }
204
205    if prev_end.weekday().number_from_monday() > 5 {
206        prev_end -= Duration::days(i64::from(prev_end.weekday().number_from_monday()) - 5);
207    }
208
209    prev_end.with_timezone(&Utc)
210}
211
212#[cfg(test)]
213mod tests {
214    use rstest::rstest;
215
216    use super::*;
217
218    #[rstest]
219    #[case(ForexSession::Sydney, "1970-01-01T10:00:00+10:00")]
220    #[case(ForexSession::Tokyo, "1970-01-01T09:00:00+09:00")]
221    #[case(ForexSession::London, "1970-01-01T01:00:00+01:00")]
222    #[case(ForexSession::NewYork, "1969-12-31T19:00:00-05:00")]
223    pub fn test_fx_local_from_utc(#[case] session: ForexSession, #[case] expected: &str) {
224        let unix_epoch = Utc.timestamp_opt(0, 0).unwrap();
225        let result = fx_local_from_utc(session, unix_epoch);
226        assert_eq!(result.to_rfc3339(), expected);
227    }
228
229    #[rstest]
230    #[case(ForexSession::Sydney, "1970-01-01T21:00:00+00:00")]
231    #[case(ForexSession::Tokyo, "1970-01-01T00:00:00+00:00")]
232    #[case(ForexSession::London, "1970-01-01T07:00:00+00:00")]
233    #[case(ForexSession::NewYork, "1970-01-01T13:00:00+00:00")]
234    pub fn test_fx_next_start(#[case] session: ForexSession, #[case] expected: &str) {
235        let unix_epoch = Utc.timestamp_opt(0, 0).unwrap();
236        let result = fx_next_start(session, unix_epoch);
237        assert_eq!(result.to_rfc3339(), expected);
238    }
239
240    #[rstest]
241    #[case(ForexSession::Sydney, "1969-12-31T21:00:00+00:00")]
242    #[case(ForexSession::Tokyo, "1970-01-01T00:00:00+00:00")]
243    #[case(ForexSession::London, "1969-12-31T07:00:00+00:00")]
244    #[case(ForexSession::NewYork, "1969-12-31T13:00:00+00:00")]
245    pub fn test_fx_prev_start(#[case] session: ForexSession, #[case] expected: &str) {
246        let unix_epoch = Utc.timestamp_opt(0, 0).unwrap();
247        let result = fx_prev_start(session, unix_epoch);
248        assert_eq!(result.to_rfc3339(), expected);
249    }
250
251    #[rstest]
252    #[case(ForexSession::Sydney, "1970-01-01T06:00:00+00:00")]
253    #[case(ForexSession::Tokyo, "1970-01-01T09:00:00+00:00")]
254    #[case(ForexSession::London, "1970-01-01T15:00:00+00:00")]
255    #[case(ForexSession::NewYork, "1970-01-01T22:00:00+00:00")]
256    pub fn test_fx_next_end(#[case] session: ForexSession, #[case] expected: &str) {
257        let unix_epoch = Utc.timestamp_opt(0, 0).unwrap();
258        let result = fx_next_end(session, unix_epoch);
259        assert_eq!(result.to_rfc3339(), expected);
260    }
261
262    #[rstest]
263    #[case(ForexSession::Sydney, "1969-12-31T06:00:00+00:00")]
264    #[case(ForexSession::Tokyo, "1969-12-31T09:00:00+00:00")]
265    #[case(ForexSession::London, "1969-12-31T15:00:00+00:00")]
266    #[case(ForexSession::NewYork, "1969-12-31T22:00:00+00:00")]
267    pub fn test_fx_prev_end(#[case] session: ForexSession, #[case] expected: &str) {
268        let unix_epoch = Utc.timestamp_opt(0, 0).unwrap();
269        let result = fx_prev_end(session, unix_epoch);
270        assert_eq!(result.to_rfc3339(), expected);
271    }
272
273    #[rstest]
274    pub fn test_fx_next_start_on_weekend() {
275        let sunday_utc = Utc.with_ymd_and_hms(2020, 7, 12, 9, 0, 0).unwrap(); // Sunday
276        let result = fx_next_start(ForexSession::Tokyo, sunday_utc);
277        let expected = Utc.with_ymd_and_hms(2020, 7, 13, 0, 0, 0).unwrap(); // Monday
278
279        assert_eq!(result, expected);
280    }
281
282    #[rstest]
283    pub fn test_fx_next_start_during_active_session() {
284        let during_session = Utc.with_ymd_and_hms(2020, 7, 13, 10, 0, 0).unwrap(); // Sydney session is active
285        let result = fx_next_start(ForexSession::Sydney, during_session);
286        let expected = Utc.with_ymd_and_hms(2020, 7, 13, 21, 0, 0).unwrap(); // Next Sydney session start
287
288        assert_eq!(result, expected);
289    }
290
291    #[rstest]
292    pub fn test_fx_prev_start_before_session() {
293        let before_session = Utc.with_ymd_and_hms(2020, 7, 13, 6, 0, 0).unwrap(); // Before Tokyo session start
294        let result = fx_prev_start(ForexSession::Tokyo, before_session);
295        let expected = Utc.with_ymd_and_hms(2020, 7, 13, 0, 0, 0).unwrap(); // Current Tokyo session start
296
297        assert_eq!(result, expected);
298    }
299
300    #[rstest]
301    pub fn test_fx_next_end_crossing_midnight() {
302        let late_night = Utc.with_ymd_and_hms(2020, 7, 13, 23, 0, 0).unwrap(); // After NY session ended
303        let result = fx_next_end(ForexSession::NewYork, late_night);
304        let expected = Utc.with_ymd_and_hms(2020, 7, 14, 21, 0, 0).unwrap(); // Next NY session end
305
306        assert_eq!(result, expected);
307    }
308
309    #[rstest]
310    pub fn test_fx_prev_end_after_session() {
311        let after_session = Utc.with_ymd_and_hms(2020, 7, 13, 17, 30, 0).unwrap(); // Just after NY session ended
312        let result = fx_prev_end(ForexSession::NewYork, after_session);
313        let expected = Utc.with_ymd_and_hms(2020, 7, 10, 21, 0, 0).unwrap(); // Previous NY session end
314
315        assert_eq!(result, expected);
316    }
317}