1use 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#[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(eq, eq_int, module = "nautilus_trader.core.nautilus_pyo3.common.enums")
38)]
39pub enum ForexSession {
40 Sydney,
41 Tokyo,
42 London,
43 NewYork,
44}
45
46impl ForexSession {
47 const fn timezone(&self) -> Tz {
49 match self {
50 Self::Sydney => Sydney,
51 Self::Tokyo => Tokyo,
52 Self::London => London,
53 Self::NewYork => New_York,
54 }
55 }
56
57 const fn session_times(&self) -> (NaiveTime, NaiveTime) {
59 match self {
60 Self::Sydney => (
61 NaiveTime::from_hms_opt(7, 0, 0).unwrap(),
62 NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
63 ),
64 Self::Tokyo => (
65 NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
66 NaiveTime::from_hms_opt(18, 0, 0).unwrap(),
67 ),
68 Self::London => (
69 NaiveTime::from_hms_opt(8, 0, 0).unwrap(),
70 NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
71 ),
72 Self::NewYork => (
73 NaiveTime::from_hms_opt(8, 0, 0).unwrap(),
74 NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
75 ),
76 }
77 }
78}
79
80#[must_use]
82pub fn fx_local_from_utc(session: ForexSession, time_now: DateTime<Utc>) -> DateTime<Tz> {
83 session.timezone().from_utc_datetime(&time_now.naive_utc())
84}
85
86#[must_use]
88pub fn fx_next_start(session: ForexSession, time_now: DateTime<Utc>) -> DateTime<Utc> {
89 let timezone = session.timezone();
90 let local_now = fx_local_from_utc(session, time_now);
91 let (start_time, _) = session.session_times();
92
93 let mut next_start = timezone
94 .with_ymd_and_hms(
95 local_now.year(),
96 local_now.month(),
97 local_now.day(),
98 start_time.hour(),
99 start_time.minute(),
100 0,
101 )
102 .unwrap();
103
104 if local_now > next_start {
105 next_start += Duration::days(1);
106 }
107
108 if next_start.weekday().number_from_monday() > 5 {
109 next_start += Duration::days(8 - i64::from(next_start.weekday().number_from_monday()));
110 }
111
112 next_start.with_timezone(&Utc)
113}
114
115#[must_use]
117pub fn fx_prev_start(session: ForexSession, time_now: DateTime<Utc>) -> DateTime<Utc> {
118 let timezone = session.timezone();
119 let local_now = fx_local_from_utc(session, time_now);
120 let (start_time, _) = session.session_times();
121
122 let mut prev_start = timezone
123 .with_ymd_and_hms(
124 local_now.year(),
125 local_now.month(),
126 local_now.day(),
127 start_time.hour(),
128 start_time.minute(),
129 0,
130 )
131 .unwrap();
132
133 if local_now < prev_start {
134 prev_start -= Duration::days(1);
135 }
136
137 if prev_start.weekday().number_from_monday() > 5 {
138 prev_start -= Duration::days(i64::from(prev_start.weekday().number_from_monday()) - 5);
139 }
140
141 prev_start.with_timezone(&Utc)
142}
143
144#[must_use]
146pub fn fx_next_end(session: ForexSession, time_now: DateTime<Utc>) -> DateTime<Utc> {
147 let timezone = session.timezone();
148 let local_now = fx_local_from_utc(session, time_now);
149 let (_, end_time) = session.session_times();
150
151 let mut next_end = timezone
152 .with_ymd_and_hms(
153 local_now.year(),
154 local_now.month(),
155 local_now.day(),
156 end_time.hour(),
157 end_time.minute(),
158 0,
159 )
160 .unwrap();
161
162 if local_now > next_end {
163 next_end += Duration::days(1);
164 }
165
166 if next_end.weekday().number_from_monday() > 5 {
167 next_end += Duration::days(8 - i64::from(next_end.weekday().number_from_monday()));
168 }
169
170 next_end.with_timezone(&Utc)
171}
172
173#[must_use]
175pub fn fx_prev_end(session: ForexSession, time_now: DateTime<Utc>) -> DateTime<Utc> {
176 let timezone = session.timezone();
177 let local_now = fx_local_from_utc(session, time_now);
178 let (_, end_time) = session.session_times();
179
180 let mut prev_end = timezone
181 .with_ymd_and_hms(
182 local_now.year(),
183 local_now.month(),
184 local_now.day(),
185 end_time.hour(),
186 end_time.minute(),
187 0,
188 )
189 .unwrap();
190
191 if local_now < prev_end {
192 prev_end -= Duration::days(1);
193 }
194
195 if prev_end.weekday().number_from_monday() > 5 {
196 prev_end -= Duration::days(i64::from(prev_end.weekday().number_from_monday()) - 5);
197 }
198
199 prev_end.with_timezone(&Utc)
200}
201
202#[cfg(test)]
206mod tests {
207 use rstest::rstest;
208
209 use super::*;
210
211 #[rstest]
212 #[case(ForexSession::Sydney, "1970-01-01T10:00:00+10:00")]
213 #[case(ForexSession::Tokyo, "1970-01-01T09:00:00+09:00")]
214 #[case(ForexSession::London, "1970-01-01T01:00:00+01:00")]
215 #[case(ForexSession::NewYork, "1969-12-31T19:00:00-05:00")]
216 pub fn test_fx_local_from_utc(#[case] session: ForexSession, #[case] expected: &str) {
217 let unix_epoch = Utc.timestamp_opt(0, 0).unwrap();
218 let result = fx_local_from_utc(session, unix_epoch);
219 assert_eq!(result.to_rfc3339(), expected);
220 }
221
222 #[rstest]
223 #[case(ForexSession::Sydney, "1970-01-01T21:00:00+00:00")]
224 #[case(ForexSession::Tokyo, "1970-01-01T00:00:00+00:00")]
225 #[case(ForexSession::London, "1970-01-01T07:00:00+00:00")]
226 #[case(ForexSession::NewYork, "1970-01-01T13:00:00+00:00")]
227 pub fn test_fx_next_start(#[case] session: ForexSession, #[case] expected: &str) {
228 let unix_epoch = Utc.timestamp_opt(0, 0).unwrap();
229 let result = fx_next_start(session, unix_epoch);
230 assert_eq!(result.to_rfc3339(), expected);
231 }
232
233 #[rstest]
234 #[case(ForexSession::Sydney, "1969-12-31T21:00:00+00:00")]
235 #[case(ForexSession::Tokyo, "1970-01-01T00:00:00+00:00")]
236 #[case(ForexSession::London, "1969-12-31T07:00:00+00:00")]
237 #[case(ForexSession::NewYork, "1969-12-31T13:00:00+00:00")]
238 pub fn test_fx_prev_start(#[case] session: ForexSession, #[case] expected: &str) {
239 let unix_epoch = Utc.timestamp_opt(0, 0).unwrap();
240 let result = fx_prev_start(session, unix_epoch);
241 assert_eq!(result.to_rfc3339(), expected);
242 }
243
244 #[rstest]
245 #[case(ForexSession::Sydney, "1970-01-01T06:00:00+00:00")]
246 #[case(ForexSession::Tokyo, "1970-01-01T09:00:00+00:00")]
247 #[case(ForexSession::London, "1970-01-01T15:00:00+00:00")]
248 #[case(ForexSession::NewYork, "1970-01-01T22:00:00+00:00")]
249 pub fn test_fx_next_end(#[case] session: ForexSession, #[case] expected: &str) {
250 let unix_epoch = Utc.timestamp_opt(0, 0).unwrap();
251 let result = fx_next_end(session, unix_epoch);
252 assert_eq!(result.to_rfc3339(), expected);
253 }
254
255 #[rstest]
256 #[case(ForexSession::Sydney, "1969-12-31T06:00:00+00:00")]
257 #[case(ForexSession::Tokyo, "1969-12-31T09:00:00+00:00")]
258 #[case(ForexSession::London, "1969-12-31T15:00:00+00:00")]
259 #[case(ForexSession::NewYork, "1969-12-31T22:00:00+00:00")]
260 pub fn test_fx_prev_end(#[case] session: ForexSession, #[case] expected: &str) {
261 let unix_epoch = Utc.timestamp_opt(0, 0).unwrap();
262 let result = fx_prev_end(session, unix_epoch);
263 assert_eq!(result.to_rfc3339(), expected);
264 }
265
266 #[rstest]
267 pub fn test_fx_next_start_on_weekend() {
268 let sunday_utc = Utc.with_ymd_and_hms(2020, 7, 12, 9, 0, 0).unwrap(); let result = fx_next_start(ForexSession::Tokyo, sunday_utc);
270 let expected = Utc.with_ymd_and_hms(2020, 7, 13, 0, 0, 0).unwrap(); assert_eq!(result, expected);
273 }
274
275 #[rstest]
276 pub fn test_fx_next_start_during_active_session() {
277 let during_session = Utc.with_ymd_and_hms(2020, 7, 13, 10, 0, 0).unwrap(); let result = fx_next_start(ForexSession::Sydney, during_session);
279 let expected = Utc.with_ymd_and_hms(2020, 7, 13, 21, 0, 0).unwrap(); assert_eq!(result, expected);
282 }
283
284 #[rstest]
285 pub fn test_fx_prev_start_before_session() {
286 let before_session = Utc.with_ymd_and_hms(2020, 7, 13, 6, 0, 0).unwrap(); let result = fx_prev_start(ForexSession::Tokyo, before_session);
288 let expected = Utc.with_ymd_and_hms(2020, 7, 13, 0, 0, 0).unwrap(); assert_eq!(result, expected);
291 }
292
293 #[rstest]
294 pub fn test_fx_next_end_crossing_midnight() {
295 let late_night = Utc.with_ymd_and_hms(2020, 7, 13, 23, 0, 0).unwrap(); let result = fx_next_end(ForexSession::NewYork, late_night);
297 let expected = Utc.with_ymd_and_hms(2020, 7, 14, 21, 0, 0).unwrap(); assert_eq!(result, expected);
300 }
301
302 #[rstest]
303 pub fn test_fx_prev_end_after_session() {
304 let after_session = Utc.with_ymd_and_hms(2020, 7, 13, 17, 30, 0).unwrap(); let result = fx_prev_end(ForexSession::NewYork, after_session);
306 let expected = Utc.with_ymd_and_hms(2020, 7, 10, 21, 0, 0).unwrap(); assert_eq!(result, expected);
309 }
310}