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(
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 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 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#[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#[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#[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#[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#[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(); let result = fx_next_start(ForexSession::Tokyo, sunday_utc);
277 let expected = Utc.with_ymd_and_hms(2020, 7, 13, 0, 0, 0).unwrap(); 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(); let result = fx_next_start(ForexSession::Sydney, during_session);
286 let expected = Utc.with_ymd_and_hms(2020, 7, 13, 21, 0, 0).unwrap(); 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(); let result = fx_prev_start(ForexSession::Tokyo, before_session);
295 let expected = Utc.with_ymd_and_hms(2020, 7, 13, 0, 0, 0).unwrap(); 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(); let result = fx_next_end(ForexSession::NewYork, late_night);
304 let expected = Utc.with_ymd_and_hms(2020, 7, 14, 21, 0, 0).unwrap(); 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(); let result = fx_prev_end(ForexSession::NewYork, after_session);
313 let expected = Utc.with_ymd_and_hms(2020, 7, 10, 21, 0, 0).unwrap(); assert_eq!(result, expected);
316 }
317}