1use chrono::{DateTime, Datelike, Duration, NaiveDate, NaiveTime, Utc};
9use std::collections::BTreeSet;
10use std::sync::Arc;
11
12use crate::holiday::{HolidayRule, Weekday, WeekendRoll};
13use crate::range::STANDARD_WEEKMASK;
14use crate::trading_hours::{ExtendedSession, Session, TradingHours};
15
16pub use finance_enums::data::{
17 AgricultureType_VARIANTS as AGRICULTURE_TYPES, CommodityType_VARIANTS as COMMODITY_TYPES,
18 CountryCode3_VARIANTS as COUNTRY_CODES3, CountryCode_VARIANTS as COUNTRY_CODES,
19 EnergyType_VARIANTS as ENERGY_TYPES, ExchangeCode_VARIANTS as EXCHANGE_CODES,
20 MarketType_VARIANTS as MARKET_TYPES, MetalsType_VARIANTS as METALS_TYPES,
21 UnderlyingAssetClass_VARIANTS as UNDERLYING_ASSET_CLASSES,
22};
23
24pub const CRYPTO_WEEKMASK: [bool; 7] = [true, true, true, true, true, true, true];
26
27pub const FX_WEEKMASK: [bool; 7] = [true, true, true, true, true, false, true];
29
30const MARKET_TYPE_ENUM: &str = "MarketType";
31
32fn finance_enum_variant(
33 enum_name: &str,
34 variants: &'static [&'static str],
35 variant: &str,
36) -> &'static str {
37 variants
38 .iter()
39 .copied()
40 .find(|candidate| *candidate == variant)
41 .unwrap_or_else(|| panic!("finance-enums missing {enum_name}.{variant}"))
42}
43
44fn market_type(variant: &str) -> &'static str {
45 finance_enum_variant(MARKET_TYPE_ENUM, MARKET_TYPES, variant)
46}
47
48#[derive(Clone, Debug)]
50pub struct CalendarSchedule {
51 pub effective_start: NaiveDate,
52 pub weekmask: [bool; 7],
53 pub rules: Vec<HolidayRule>,
54 pub trading_hours: Option<TradingHours>,
55}
56
57impl CalendarSchedule {
58 pub fn new(
59 effective_start: NaiveDate,
60 weekmask: [bool; 7],
61 rules: Vec<HolidayRule>,
62 trading_hours: Option<TradingHours>,
63 ) -> Self {
64 Self {
65 effective_start,
66 weekmask,
67 rules,
68 trading_hours,
69 }
70 }
71}
72
73struct ResolvedSchedule<'a> {
74 weekmask: &'a [bool; 7],
75 rules: &'a [HolidayRule],
76 trading_hours: Option<&'a TradingHours>,
77}
78
79pub struct Calendar {
81 pub name: String,
82 pub market_type: &'static str,
84 pub weekmask: [bool; 7],
85 pub rules: Vec<HolidayRule>,
86 pub trading_hours: Option<TradingHours>,
87 pub schedules: Vec<CalendarSchedule>,
88 pub early_closes: Vec<EarlyCloseRule>,
92 cache: HolidayCache,
93 early_cache: EarlyCloseCache,
94}
95
96#[derive(Clone, Debug)]
100pub struct EarlyCloseRule {
101 pub rule: HolidayRule,
102 pub close_time: NaiveTime,
103}
104
105#[derive(Default)]
106struct EarlyCloseCache {
107 inner: parking_lot_dummy::RwLock<
108 std::collections::HashMap<i32, Arc<std::collections::HashMap<NaiveDate, NaiveTime>>>,
109 >,
110}
111
112#[derive(Default)]
113struct HolidayCache {
114 inner: parking_lot_dummy::RwLock<std::collections::HashMap<i32, Arc<BTreeSet<NaiveDate>>>>,
115}
116
117mod parking_lot_dummy {
118 use std::sync::RwLock as StdRwLock;
119 pub struct RwLock<T>(pub StdRwLock<T>);
120 impl<T: Default> Default for RwLock<T> {
121 fn default() -> Self {
122 Self(StdRwLock::new(T::default()))
123 }
124 }
125 impl<T> RwLock<T> {
126 pub fn read(&self) -> std::sync::RwLockReadGuard<'_, T> {
127 self.0.read().unwrap()
128 }
129 pub fn write(&self) -> std::sync::RwLockWriteGuard<'_, T> {
130 self.0.write().unwrap()
131 }
132 }
133}
134
135impl Calendar {
136 pub fn new(
137 name: impl Into<String>,
138 weekmask: [bool; 7],
139 rules: Vec<HolidayRule>,
140 trading_hours: Option<TradingHours>,
141 ) -> Self {
142 Self::with_type(
143 name,
144 market_type("Equities"),
145 weekmask,
146 rules,
147 trading_hours,
148 )
149 }
150
151 pub fn with_type(
152 name: impl Into<String>,
153 market_type: &'static str,
154 weekmask: [bool; 7],
155 rules: Vec<HolidayRule>,
156 trading_hours: Option<TradingHours>,
157 ) -> Self {
158 Self {
159 name: name.into(),
160 market_type,
161 weekmask,
162 rules,
163 trading_hours,
164 schedules: Vec::new(),
165 early_closes: Vec::new(),
166 cache: HolidayCache::default(),
167 early_cache: EarlyCloseCache::default(),
168 }
169 }
170
171 pub fn with_early_closes(mut self, ec: Vec<EarlyCloseRule>) -> Self {
173 self.early_closes = ec;
174 self
175 }
176
177 pub fn with_schedules(mut self, mut schedules: Vec<CalendarSchedule>) -> Self {
179 schedules.sort_by_key(|s| s.effective_start);
180 self.schedules = schedules;
181 self.cache = HolidayCache::default();
182 self.early_cache = EarlyCloseCache::default();
183 self
184 }
185
186 fn schedule_for(&self, date: NaiveDate) -> ResolvedSchedule<'_> {
187 let mut selected = None;
188 for schedule in &self.schedules {
189 if schedule.effective_start <= date {
190 selected = Some(schedule);
191 } else {
192 break;
193 }
194 }
195 match selected {
196 Some(schedule) => ResolvedSchedule {
197 weekmask: &schedule.weekmask,
198 rules: &schedule.rules,
199 trading_hours: schedule.trading_hours.as_ref(),
200 },
201 None => ResolvedSchedule {
202 weekmask: &self.weekmask,
203 rules: &self.rules,
204 trading_hours: self.trading_hours.as_ref(),
205 },
206 }
207 }
208
209 fn is_holiday_uncached(&self, date: NaiveDate) -> bool {
210 let schedule = self.schedule_for(date);
211 schedule
212 .rules
213 .iter()
214 .flat_map(|rule| rule.dates_in(date.year()))
215 .any(|holiday| holiday == date)
216 }
217
218 fn early_close_map(&self, year: i32) -> Arc<std::collections::HashMap<NaiveDate, NaiveTime>> {
220 if let Some(m) = self.early_cache.inner.read().get(&year).cloned() {
221 return m;
222 }
223 let mut m = std::collections::HashMap::new();
224 for ec in &self.early_closes {
225 if let Some(d) = ec.rule.observed_in(year) {
226 let i = d.weekday().num_days_from_monday() as usize;
229 if !self.schedule_for(d).weekmask[i] {
230 continue;
231 }
232 if self.holidays(year).contains(&d) {
233 continue;
234 }
235 m.insert(d, ec.close_time);
236 }
237 }
238 let arc = Arc::new(m);
239 self.early_cache.inner.write().insert(year, arc.clone());
240 arc
241 }
242
243 pub fn early_close_for(&self, date: NaiveDate) -> Option<NaiveTime> {
245 self.early_close_map(date.year()).get(&date).copied()
246 }
247
248 pub fn holidays(&self, year: i32) -> Arc<BTreeSet<NaiveDate>> {
249 if let Some(h) = self.cache.inner.read().get(&year).cloned() {
250 return h;
251 }
252 let mut set = BTreeSet::new();
253 if let Some(mut d) = NaiveDate::from_ymd_opt(year, 1, 1) {
254 while d.year() == year {
255 if self.is_holiday_uncached(d) {
256 set.insert(d);
257 }
258 d += Duration::days(1);
259 }
260 }
261 let arc = Arc::new(set);
262 self.cache.inner.write().insert(year, arc.clone());
263 arc
264 }
265
266 pub fn holidays_between(&self, start: NaiveDate, end: NaiveDate) -> BTreeSet<NaiveDate> {
267 let mut out = BTreeSet::new();
268 for y in start.year()..=end.year() {
269 for d in self.holidays(y).iter() {
270 if *d >= start && *d <= end {
271 out.insert(*d);
272 }
273 }
274 }
275 out
276 }
277
278 pub fn is_holiday(&self, d: NaiveDate) -> bool {
279 self.holidays(d.year()).contains(&d)
280 }
281
282 pub fn is_business_day(&self, d: NaiveDate) -> bool {
283 let i = d.weekday().num_days_from_monday() as usize;
284 self.schedule_for(d).weekmask[i] && !self.is_holiday(d)
285 }
286
287 pub fn next_business_day(&self, d: NaiveDate) -> NaiveDate {
288 let mut x = d + Duration::days(1);
289 loop {
290 if self.is_business_day(x) {
291 return x;
292 }
293 x += Duration::days(1);
294 }
295 }
296
297 pub fn previous_business_day(&self, d: NaiveDate) -> NaiveDate {
298 let mut x = d - Duration::days(1);
299 loop {
300 if self.is_business_day(x) {
301 return x;
302 }
303 x -= Duration::days(1);
304 }
305 }
306
307 pub fn business_days_between(&self, start: NaiveDate, end: NaiveDate) -> i64 {
308 if end < start {
309 return 0;
310 }
311 let mut n = 0;
312 let mut d = start;
313 while d <= end {
314 if self.is_business_day(d) {
315 n += 1;
316 }
317 d += Duration::days(1);
318 }
319 n
320 }
321
322 pub fn business_day_range(&self, start: NaiveDate, end: NaiveDate) -> Vec<NaiveDate> {
323 if end < start {
324 return Vec::new();
325 }
326 let mut out = Vec::with_capacity(((end - start).num_days() as usize).saturating_add(1));
327 let mut d = start;
328 while d <= end {
329 if self.is_business_day(d) {
330 out.push(d);
331 }
332 d += Duration::days(1);
333 }
334 out
335 }
336
337 pub fn is_open(&self, when: DateTime<Utc>) -> bool {
345 let Some(th) = &self.trading_hours else {
346 return false;
347 };
348 let local_today = when.with_timezone(&th.timezone).date_naive();
349 for delta in [0i64, 1] {
350 let trading_day = local_today + Duration::days(delta);
351 if !self.is_business_day(trading_day) {
352 continue;
353 }
354 let Some(th) = self.schedule_for(trading_day).trading_hours else {
355 continue;
356 };
357 let early = self.early_close_for(trading_day);
358 let last_idx = th.sessions.len().saturating_sub(1);
359 for (i, s) in th.sessions.iter().enumerate() {
360 let Some((o, mut c)) = s.instants(th.timezone, trading_day) else {
361 continue;
362 };
363 if i == last_idx {
364 if let Some(t) = early {
365 if let Some(early_c) = adjust_close(th.timezone, trading_day, s, t) {
366 c = early_c;
367 }
368 }
369 }
370 if when >= o && when < c {
371 return true;
372 }
373 }
374 }
375 false
376 }
377
378 pub fn next_open(&self, when: DateTime<Utc>) -> Option<DateTime<Utc>> {
379 let th = self.trading_hours.as_ref()?;
380 let local_today = when.with_timezone(&th.timezone).date_naive();
381 for delta in 0..400i64 {
382 let trading_day = local_today + Duration::days(delta);
383 if !self.is_business_day(trading_day) {
384 continue;
385 }
386 let Some(th) = self.schedule_for(trading_day).trading_hours else {
387 continue;
388 };
389 for s in &th.sessions {
390 if let Some((o, _)) = s.instants(th.timezone, trading_day) {
391 if o >= when {
392 return Some(o);
393 }
394 }
395 }
396 }
397 None
398 }
399
400 pub fn next_close(&self, when: DateTime<Utc>) -> Option<DateTime<Utc>> {
401 let th = self.trading_hours.as_ref()?;
402 let local_today = when.with_timezone(&th.timezone).date_naive();
403 for delta in 0..400i64 {
404 let trading_day = local_today + Duration::days(delta);
405 if !self.is_business_day(trading_day) {
406 continue;
407 }
408 let Some(th) = self.schedule_for(trading_day).trading_hours else {
409 continue;
410 };
411 let early = self.early_close_for(trading_day);
412 let last_idx = th.sessions.len().saturating_sub(1);
413 for (i, s) in th.sessions.iter().enumerate() {
414 let Some((_, mut c)) = s.instants(th.timezone, trading_day) else {
415 continue;
416 };
417 if i == last_idx {
418 if let Some(t) = early {
419 if let Some(early_c) = adjust_close(th.timezone, trading_day, s, t) {
420 c = early_c;
421 }
422 }
423 }
424 if c >= when {
425 return Some(c);
426 }
427 }
428 }
429 None
430 }
431
432 pub fn sessions_between(
438 &self,
439 start: NaiveDate,
440 end: NaiveDate,
441 ) -> Vec<(DateTime<Utc>, DateTime<Utc>)> {
442 let mut out = Vec::new();
443 let mut d = start;
444 while d <= end {
445 if self.is_business_day(d) {
446 let Some(th) = self.schedule_for(d).trading_hours else {
447 d += Duration::days(1);
448 continue;
449 };
450 let last_idx = th.sessions.len().saturating_sub(1);
451 let early = self.early_close_for(d);
452 for (i, s) in th.sessions.iter().enumerate() {
453 let Some((o, mut c)) = s.instants(th.timezone, d) else {
454 continue;
455 };
456 if i == last_idx {
457 if let Some(t) = early {
458 if let Some(early_c) = adjust_close(th.timezone, d, s, t) {
459 c = early_c;
460 }
461 }
462 }
463 out.push((o, c));
464 }
465 }
466 d += Duration::days(1);
467 }
468 out
469 }
470
471 pub fn extended_sessions_between(
474 &self,
475 start: NaiveDate,
476 end: NaiveDate,
477 ) -> Vec<(&'static str, DateTime<Utc>, DateTime<Utc>)> {
478 let mut out = Vec::new();
479 let mut d = start;
480 while d <= end {
481 if self.is_business_day(d) {
482 let Some(th) = self.schedule_for(d).trading_hours else {
483 d += Duration::days(1);
484 continue;
485 };
486 let early = self.early_close_for(d);
487 for s in &th.extended_sessions {
488 let Some((mut o, c)) = s.session.instants(th.timezone, d) else {
489 continue;
490 };
491 if s.name == "after_close" {
492 if let Some(t) = early {
493 if let Some(early_o) = adjust_open(th.timezone, d, &s.session, t) {
494 o = early_o;
495 }
496 }
497 }
498 if o < c {
499 out.push((s.name, o, c));
500 }
501 }
502 }
503 d += Duration::days(1);
504 }
505 out
506 }
507}
508
509fn adjust_open(
510 tz: chrono_tz::Tz,
511 trading_day: NaiveDate,
512 session: &Session,
513 local_open_time: NaiveTime,
514) -> Option<DateTime<Utc>> {
515 use chrono::TimeZone;
516 let open_local_day = trading_day + Duration::days(session.open_day_offset as i64);
517 let open = tz
518 .from_local_datetime(&open_local_day.and_time(local_open_time))
519 .single()?;
520 Some(open.with_timezone(&Utc))
521}
522
523fn adjust_close(
527 tz: chrono_tz::Tz,
528 trading_day: NaiveDate,
529 session: &Session,
530 local_close_time: NaiveTime,
531) -> Option<DateTime<Utc>> {
532 use chrono::TimeZone;
533 let close_local_day = trading_day + Duration::days(session.close_day_offset as i64);
534 let close = tz
535 .from_local_datetime(&close_local_day.and_time(local_close_time))
536 .single()?;
537 Some(close.with_timezone(&Utc))
538}
539
540fn fixed(month: u32, day: u32, since_year: Option<i32>) -> HolidayRule {
543 HolidayRule::Fixed {
544 month,
545 day,
546 roll: WeekendRoll::NearestWeekday,
547 since_year,
548 }
549}
550
551fn fixed_no_roll(month: u32, day: u32, since_year: Option<i32>) -> HolidayRule {
552 HolidayRule::Fixed {
553 month,
554 day,
555 roll: WeekendRoll::None,
556 since_year,
557 }
558}
559
560fn nth(month: u32, weekday: Weekday, n: i32) -> HolidayRule {
561 HolidayRule::NthWeekday {
562 month,
563 weekday,
564 n,
565 since_year: None,
566 }
567}
568
569fn easter(offset_days: i32) -> HolidayRule {
570 HolidayRule::EasterOffset {
571 offset_days,
572 since_year: None,
573 }
574}
575
576fn nyse_rules() -> Vec<HolidayRule> {
579 vec![
580 fixed(1, 1, None),
581 nth(1, Weekday::Mon, 3),
582 nth(2, Weekday::Mon, 3),
583 easter(-2),
584 nth(5, Weekday::Mon, -1),
585 fixed(6, 19, Some(2021)),
586 fixed(7, 4, None),
587 nth(9, Weekday::Mon, 1),
588 nth(11, Weekday::Thu, 4),
589 fixed(12, 25, None),
590 ]
591}
592
593fn nyse_trading_hours() -> TradingHours {
594 TradingHours::new(
595 NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
596 NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
597 chrono_tz::America::New_York,
598 )
599 .with_extended_sessions(vec![
600 ExtendedSession::new(
601 "pre_open",
602 Session::regular(
603 NaiveTime::from_hms_opt(4, 0, 0).unwrap(),
604 NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
605 ),
606 ),
607 ExtendedSession::new(
608 "after_close",
609 Session::regular(
610 NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
611 NaiveTime::from_hms_opt(20, 0, 0).unwrap(),
612 ),
613 ),
614 ])
615}
616
617fn options_trading_hours() -> TradingHours {
621 TradingHours::new(
622 NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
623 NaiveTime::from_hms_opt(16, 15, 0).unwrap(),
624 chrono_tz::America::New_York,
625 )
626}
627
628fn cme_globex_rules() -> Vec<HolidayRule> {
633 vec![fixed(1, 1, None), easter(-2), fixed(12, 25, None)]
634}
635
636fn cme_globex_overnight_hours() -> TradingHours {
638 TradingHours::from_sessions(
639 vec![Session::overnight(
640 NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
641 NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
642 )],
643 chrono_tz::America::Chicago,
644 )
645}
646
647fn cme_globex_energy_hours() -> TradingHours {
651 TradingHours::from_sessions(
652 vec![Session::overnight(
653 NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
654 NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
655 )],
656 chrono_tz::America::Chicago,
657 )
658}
659
660fn cbot_grain_futures_hours() -> TradingHours {
665 TradingHours::from_sessions(
666 vec![
667 Session {
668 open: NaiveTime::from_hms_opt(19, 0, 0).unwrap(),
669 open_day_offset: -1,
670 close: NaiveTime::from_hms_opt(7, 45, 0).unwrap(),
671 close_day_offset: 0,
672 },
673 Session::regular(
674 NaiveTime::from_hms_opt(8, 30, 0).unwrap(),
675 NaiveTime::from_hms_opt(13, 20, 0).unwrap(),
676 ),
677 ],
678 chrono_tz::America::Chicago,
679 )
680}
681
682fn cme_livestock_hours() -> TradingHours {
686 TradingHours::new(
687 NaiveTime::from_hms_opt(8, 30, 0).unwrap(),
688 NaiveTime::from_hms_opt(13, 5, 0).unwrap(),
689 chrono_tz::America::Chicago,
690 )
691}
692
693fn cme_lumber_hours() -> TradingHours {
696 TradingHours::new(
697 NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
698 NaiveTime::from_hms_opt(15, 5, 0).unwrap(),
699 chrono_tz::America::Chicago,
700 )
701}
702
703fn cfe_rules() -> Vec<HolidayRule> {
705 vec![
706 fixed(1, 1, None),
707 nth(1, Weekday::Mon, 3),
708 nth(2, Weekday::Mon, 3),
709 easter(-2),
710 nth(5, Weekday::Mon, -1),
711 fixed(6, 19, Some(2022)),
712 fixed(7, 4, None),
713 nth(9, Weekday::Mon, 1),
714 nth(11, Weekday::Thu, 4),
715 fixed(12, 25, None),
716 ]
717}
718
719fn cfe_trading_hours() -> TradingHours {
720 TradingHours::new(
721 NaiveTime::from_hms_opt(8, 30, 0).unwrap(),
722 NaiveTime::from_hms_opt(15, 15, 0).unwrap(),
723 chrono_tz::America::Chicago,
724 )
725}
726
727fn ice_us_rules() -> Vec<HolidayRule> {
729 vec![fixed(1, 1, None), easter(-2), fixed(12, 25, None)]
730}
731
732fn ice_us_hours() -> TradingHours {
733 TradingHours::from_sessions(
734 vec![Session::overnight(
735 NaiveTime::from_hms_opt(20, 0, 0).unwrap(),
736 NaiveTime::from_hms_opt(18, 0, 0).unwrap(),
737 )],
738 chrono_tz::America::New_York,
739 )
740}
741
742fn sifma_us_rules() -> Vec<HolidayRule> {
744 vec![
745 fixed(1, 1, None),
746 nth(1, Weekday::Mon, 3),
747 nth(2, Weekday::Mon, 3),
748 easter(-2),
749 nth(5, Weekday::Mon, -1),
750 fixed(6, 19, Some(2022)),
751 fixed(7, 4, None),
752 nth(9, Weekday::Mon, 1),
753 nth(10, Weekday::Mon, 2), fixed(11, 11, None), nth(11, Weekday::Thu, 4),
756 fixed(12, 25, None),
757 ]
758}
759
760fn sifma_us_hours() -> TradingHours {
761 TradingHours::new(
762 NaiveTime::from_hms_opt(7, 0, 0).unwrap(),
763 NaiveTime::from_hms_opt(17, 30, 0).unwrap(),
764 chrono_tz::America::New_York,
765 )
766}
767
768fn forex_rules() -> Vec<HolidayRule> {
771 vec![fixed(1, 1, None), fixed(12, 25, None)]
772}
773
774fn crypto_rules() -> Vec<HolidayRule> {
776 vec![]
777}
778
779fn lse_rules() -> Vec<HolidayRule> {
780 vec![
781 fixed(1, 1, None),
782 easter(-2),
783 easter(1),
784 nth(5, Weekday::Mon, 1),
785 nth(5, Weekday::Mon, -1),
786 nth(8, Weekday::Mon, -1),
787 fixed(12, 25, None),
788 fixed(12, 26, None),
789 ]
790}
791
792fn lse_trading_hours() -> TradingHours {
793 TradingHours::new(
794 NaiveTime::from_hms_opt(8, 0, 0).unwrap(),
795 NaiveTime::from_hms_opt(16, 30, 0).unwrap(),
796 chrono_tz::Europe::London,
797 )
798}
799
800fn tse_rules() -> Vec<HolidayRule> {
801 vec![
802 fixed_no_roll(1, 1, None),
803 fixed_no_roll(1, 2, None),
804 fixed_no_roll(1, 3, None),
805 nth(1, Weekday::Mon, 2),
806 fixed_no_roll(2, 11, None),
807 fixed_no_roll(2, 23, Some(2020)),
808 fixed_no_roll(4, 29, None),
809 fixed_no_roll(5, 3, None),
810 fixed_no_roll(5, 4, None),
811 fixed_no_roll(5, 5, None),
812 nth(7, Weekday::Mon, 3),
813 fixed_no_roll(8, 11, None),
814 nth(9, Weekday::Mon, 3),
815 nth(10, Weekday::Mon, 2),
816 fixed_no_roll(11, 3, None),
817 fixed_no_roll(11, 23, None),
818 fixed_no_roll(12, 31, None),
819 ]
820}
821
822fn tse_trading_hours_with_afternoon_close(close: NaiveTime) -> TradingHours {
823 TradingHours::from_sessions(
824 vec![
825 Session::regular(
826 NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
827 NaiveTime::from_hms_opt(11, 30, 0).unwrap(),
828 ),
829 Session::regular(NaiveTime::from_hms_opt(12, 30, 0).unwrap(), close),
830 ],
831 chrono_tz::Asia::Tokyo,
832 )
833}
834
835fn tse_trading_hours() -> TradingHours {
836 tse_trading_hours_with_afternoon_close(NaiveTime::from_hms_opt(15, 30, 0).unwrap())
839}
840
841fn tse_historical_trading_hours() -> TradingHours {
842 tse_trading_hours_with_afternoon_close(NaiveTime::from_hms_opt(15, 0, 0).unwrap())
844}
845
846fn tse_schedules() -> Vec<CalendarSchedule> {
847 vec![
848 CalendarSchedule::new(
849 NaiveDate::from_ymd_opt(1900, 1, 1).unwrap(),
850 STANDARD_WEEKMASK,
851 tse_rules(),
852 Some(tse_historical_trading_hours()),
853 ),
854 CalendarSchedule::new(
855 NaiveDate::from_ymd_opt(2024, 11, 5).unwrap(),
856 STANDARD_WEEKMASK,
857 tse_rules(),
858 Some(tse_trading_hours()),
859 ),
860 ]
861}
862
863fn hkex_rules() -> Vec<HolidayRule> {
864 let lny: &'static [(i32, u32, u32)] = &[
865 (2020, 1, 27),
866 (2021, 2, 12),
867 (2022, 2, 1),
868 (2023, 1, 23),
869 (2024, 2, 12),
870 (2025, 1, 29),
871 (2026, 2, 17),
872 (2027, 2, 8),
873 (2028, 1, 26),
874 (2029, 2, 13),
875 (2030, 2, 4),
876 ];
877 vec![
878 fixed(1, 1, None),
879 HolidayRule::Tabulated { table: lny },
880 easter(-2),
881 easter(1),
882 fixed(5, 1, None),
883 fixed(7, 1, None),
884 fixed(10, 1, None),
885 fixed(12, 25, None),
886 fixed(12, 26, None),
887 ]
888}
889
890fn hkex_trading_hours() -> TradingHours {
891 TradingHours::from_sessions(
894 vec![
895 Session::regular(
896 NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
897 NaiveTime::from_hms_opt(12, 0, 0).unwrap(),
898 ),
899 Session::regular(
900 NaiveTime::from_hms_opt(13, 0, 0).unwrap(),
901 NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
902 ),
903 ],
904 chrono_tz::Asia::Hong_Kong,
905 )
906}
907
908fn sse_rules() -> Vec<HolidayRule> {
909 let lny: &'static [(i32, u32, u32)] = &[
910 (2020, 1, 25),
911 (2021, 2, 12),
912 (2022, 2, 1),
913 (2023, 1, 22),
914 (2024, 2, 10),
915 (2025, 1, 29),
916 (2026, 2, 17),
917 (2027, 2, 6),
918 (2028, 1, 26),
919 (2029, 2, 13),
920 (2030, 2, 3),
921 ];
922 vec![
923 fixed(1, 1, None),
924 HolidayRule::Tabulated { table: lny },
925 fixed(5, 1, None),
926 fixed(10, 1, None),
927 fixed(10, 2, None),
928 fixed(10, 3, None),
929 ]
930}
931
932fn sse_trading_hours() -> TradingHours {
933 TradingHours::from_sessions(
937 vec![
938 Session::regular(
939 NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
940 NaiveTime::from_hms_opt(11, 30, 0).unwrap(),
941 ),
942 Session::regular(
943 NaiveTime::from_hms_opt(13, 0, 0).unwrap(),
944 NaiveTime::from_hms_opt(15, 0, 0).unwrap(),
945 ),
946 ],
947 chrono_tz::Asia::Shanghai,
948 )
949}
950
951fn xetra_rules() -> Vec<HolidayRule> {
952 vec![
953 fixed(1, 1, None),
954 easter(-2),
955 easter(1),
956 fixed(5, 1, None),
957 fixed(10, 3, None),
958 fixed(12, 24, None),
959 fixed(12, 25, None),
960 fixed(12, 26, None),
961 fixed(12, 31, None),
962 ]
963}
964
965fn xetra_trading_hours() -> TradingHours {
966 TradingHours::new(
967 NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
968 NaiveTime::from_hms_opt(17, 30, 0).unwrap(),
969 chrono_tz::Europe::Berlin,
970 )
971}
972
973fn euronext_paris_rules() -> Vec<HolidayRule> {
974 vec![
975 fixed(1, 1, None),
976 easter(-2),
977 easter(1),
978 fixed(5, 1, None),
979 fixed(12, 25, None),
980 fixed(12, 26, None),
981 ]
982}
983
984fn euronext_paris_trading_hours() -> TradingHours {
985 TradingHours::new(
986 NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
987 NaiveTime::from_hms_opt(17, 30, 0).unwrap(),
988 chrono_tz::Europe::Paris,
989 )
990}
991
992fn tsx_rules() -> Vec<HolidayRule> {
993 vec![
994 fixed(1, 1, None),
995 nth(2, Weekday::Mon, 3),
996 easter(-2),
997 nth(5, Weekday::Mon, -1),
998 fixed(7, 1, None),
999 nth(8, Weekday::Mon, 1),
1000 nth(9, Weekday::Mon, 1),
1001 nth(10, Weekday::Mon, 2),
1002 fixed(12, 25, None),
1003 fixed(12, 26, None),
1004 ]
1005}
1006
1007fn tsx_trading_hours() -> TradingHours {
1008 TradingHours::new(
1009 NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
1010 NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
1011 chrono_tz::America::Toronto,
1012 )
1013}
1014
1015fn asx_rules() -> Vec<HolidayRule> {
1016 vec![
1017 fixed(1, 1, None),
1018 fixed(1, 26, None),
1019 easter(-2),
1020 easter(1),
1021 fixed(4, 25, None),
1022 nth(6, Weekday::Mon, 2),
1023 fixed(12, 25, None),
1024 fixed(12, 26, None),
1025 ]
1026}
1027
1028fn asx_trading_hours() -> TradingHours {
1029 TradingHours::new(
1030 NaiveTime::from_hms_opt(10, 0, 0).unwrap(),
1031 NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
1032 chrono_tz::Australia::Sydney,
1033 )
1034}
1035
1036fn nse_rules() -> Vec<HolidayRule> {
1037 vec![
1038 fixed(1, 26, None),
1039 fixed(8, 15, None),
1040 fixed(10, 2, None),
1041 fixed(12, 25, None),
1042 ]
1043}
1044
1045fn nse_trading_hours() -> TradingHours {
1046 TradingHours::new(
1047 NaiveTime::from_hms_opt(9, 15, 0).unwrap(),
1048 NaiveTime::from_hms_opt(15, 30, 0).unwrap(),
1049 chrono_tz::Asia::Kolkata,
1050 )
1051}
1052
1053fn ec(rule: HolidayRule, h: u32, m: u32) -> EarlyCloseRule {
1056 EarlyCloseRule {
1057 rule,
1058 close_time: NaiveTime::from_hms_opt(h, m, 0).unwrap(),
1059 }
1060}
1061
1062fn nyse_early_closes() -> Vec<EarlyCloseRule> {
1069 static BLACK_FRIDAY: &[(i32, u32, u32)] = &[
1072 (2020, 11, 27),
1073 (2021, 11, 26),
1074 (2022, 11, 25),
1075 (2023, 11, 24),
1076 (2024, 11, 29),
1077 (2025, 11, 28),
1078 (2026, 11, 27),
1079 (2027, 11, 26),
1080 (2028, 11, 24),
1081 (2029, 11, 23),
1082 (2030, 11, 29),
1083 (2031, 11, 28),
1084 (2032, 11, 26),
1085 (2033, 11, 25),
1086 (2034, 11, 24),
1087 (2035, 11, 23),
1088 ];
1089 vec![
1090 ec(
1091 HolidayRule::Tabulated {
1092 table: BLACK_FRIDAY,
1093 },
1094 13,
1095 0,
1096 ),
1097 ec(fixed_no_roll(12, 24, None), 13, 0),
1098 ec(fixed_no_roll(7, 3, None), 13, 0),
1099 ]
1100}
1101
1102fn euro_basic_rules() -> Vec<HolidayRule> {
1107 vec![
1108 fixed(1, 1, None),
1109 easter(-2),
1110 easter(1),
1111 fixed(5, 1, None),
1112 fixed(12, 25, None),
1113 fixed(12, 26, None),
1114 ]
1115}
1116
1117fn euronext_hours(tz: chrono_tz::Tz) -> TradingHours {
1119 TradingHours::new(
1120 NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1121 NaiveTime::from_hms_opt(17, 30, 0).unwrap(),
1122 tz,
1123 )
1124}
1125
1126fn xams_rules() -> Vec<HolidayRule> {
1127 vec![
1130 fixed(1, 1, None),
1131 easter(-2),
1132 easter(1),
1133 fixed_no_roll(4, 27, Some(2014)),
1134 easter(39),
1135 easter(50),
1136 fixed(12, 25, None),
1137 fixed(12, 26, None),
1138 ]
1139}
1140
1141fn xbru_rules() -> Vec<HolidayRule> {
1142 vec![
1145 fixed(1, 1, None),
1146 easter(-2),
1147 easter(1),
1148 fixed(5, 1, None),
1149 easter(39),
1150 easter(50),
1151 fixed(12, 25, None),
1152 fixed(12, 26, None),
1153 ]
1154}
1155
1156fn xlis_rules() -> Vec<HolidayRule> {
1157 let mut r = euro_basic_rules();
1159 r.push(easter(-47));
1160 r
1161}
1162
1163fn xmil_rules() -> Vec<HolidayRule> {
1164 vec![
1168 fixed(1, 1, None),
1169 fixed_no_roll(1, 6, None),
1170 easter(-2),
1171 easter(1),
1172 fixed_no_roll(4, 25, None),
1173 fixed(5, 1, None),
1174 fixed_no_roll(6, 2, None),
1175 fixed_no_roll(8, 15, None),
1176 fixed_no_roll(11, 1, None),
1177 fixed_no_roll(12, 8, None),
1178 fixed(12, 25, None),
1179 fixed(12, 26, None),
1180 ]
1181}
1182
1183fn xmil_hours() -> TradingHours {
1184 TradingHours::new(
1185 NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1186 NaiveTime::from_hms_opt(17, 30, 0).unwrap(),
1187 chrono_tz::Europe::Rome,
1188 )
1189}
1190
1191fn xmad_rules() -> Vec<HolidayRule> {
1192 vec![
1196 fixed(1, 1, None),
1197 fixed_no_roll(1, 6, None),
1198 easter(-2),
1199 easter(1),
1200 fixed(5, 1, None),
1201 fixed_no_roll(8, 15, None),
1202 fixed_no_roll(10, 12, None),
1203 fixed_no_roll(11, 1, None),
1204 fixed_no_roll(12, 6, None),
1205 fixed_no_roll(12, 8, None),
1206 fixed(12, 25, None),
1207 fixed(12, 26, None),
1208 ]
1209}
1210
1211fn xmad_hours() -> TradingHours {
1212 TradingHours::new(
1213 NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1214 NaiveTime::from_hms_opt(17, 30, 0).unwrap(),
1215 chrono_tz::Europe::Madrid,
1216 )
1217}
1218
1219fn xswx_rules() -> Vec<HolidayRule> {
1220 vec![
1223 fixed(1, 1, None),
1224 fixed_no_roll(1, 2, None),
1225 easter(-2),
1226 easter(1),
1227 fixed(5, 1, None),
1228 easter(39),
1229 easter(50),
1230 fixed_no_roll(8, 1, None),
1231 fixed(12, 25, None),
1232 fixed(12, 26, None),
1233 ]
1234}
1235
1236fn xswx_hours() -> TradingHours {
1237 TradingHours::new(
1238 NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1239 NaiveTime::from_hms_opt(17, 30, 0).unwrap(),
1240 chrono_tz::Europe::Zurich,
1241 )
1242}
1243
1244fn xosl_rules() -> Vec<HolidayRule> {
1245 vec![
1249 fixed(1, 1, None),
1250 easter(-3),
1251 easter(-2),
1252 easter(1),
1253 fixed(5, 1, None),
1254 fixed_no_roll(5, 17, None),
1255 easter(39),
1256 easter(50),
1257 fixed(12, 25, None),
1258 fixed(12, 26, None),
1259 ]
1260}
1261
1262fn xosl_hours() -> TradingHours {
1263 TradingHours::new(
1264 NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1265 NaiveTime::from_hms_opt(16, 20, 0).unwrap(),
1266 chrono_tz::Europe::Oslo,
1267 )
1268}
1269
1270fn xsto_rules() -> Vec<HolidayRule> {
1271 vec![
1275 fixed(1, 1, None),
1276 fixed_no_roll(1, 6, None),
1277 easter(-2),
1278 easter(1),
1279 fixed(5, 1, None),
1280 easter(39),
1281 fixed_no_roll(6, 6, None),
1282 fixed_no_roll(12, 24, None),
1283 fixed(12, 25, None),
1284 fixed(12, 26, None),
1285 fixed_no_roll(12, 31, None),
1286 ]
1287}
1288
1289fn xsto_hours() -> TradingHours {
1290 TradingHours::new(
1291 NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1292 NaiveTime::from_hms_opt(17, 30, 0).unwrap(),
1293 chrono_tz::Europe::Stockholm,
1294 )
1295}
1296
1297fn xhel_rules() -> Vec<HolidayRule> {
1298 vec![
1302 fixed(1, 1, None),
1303 fixed_no_roll(1, 6, None),
1304 easter(-2),
1305 easter(1),
1306 fixed(5, 1, None),
1307 easter(39),
1308 fixed_no_roll(12, 6, None),
1309 fixed_no_roll(12, 24, None),
1310 fixed(12, 25, None),
1311 fixed(12, 26, None),
1312 ]
1313}
1314
1315fn xhel_hours() -> TradingHours {
1316 TradingHours::new(
1317 NaiveTime::from_hms_opt(10, 0, 0).unwrap(),
1318 NaiveTime::from_hms_opt(18, 30, 0).unwrap(),
1319 chrono_tz::Europe::Helsinki,
1320 )
1321}
1322
1323fn xcse_rules() -> Vec<HolidayRule> {
1324 vec![
1328 fixed(1, 1, None),
1329 easter(-3),
1330 easter(-2),
1331 easter(1),
1332 easter(39),
1333 fixed_no_roll(6, 5, None),
1334 fixed_no_roll(12, 24, None),
1335 fixed(12, 25, None),
1336 fixed(12, 26, None),
1337 ]
1338}
1339
1340fn xcse_hours() -> TradingHours {
1341 TradingHours::new(
1342 NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1343 NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
1344 chrono_tz::Europe::Copenhagen,
1345 )
1346}
1347
1348fn xice_rules() -> Vec<HolidayRule> {
1349 vec![
1353 fixed(1, 1, None),
1354 easter(-3),
1355 easter(-2),
1356 easter(1),
1357 fixed(5, 1, None),
1358 easter(39),
1359 easter(50),
1360 fixed_no_roll(6, 17, None),
1361 fixed_no_roll(12, 24, None),
1362 fixed(12, 25, None),
1363 fixed(12, 26, None),
1364 ]
1365}
1366
1367fn xice_hours() -> TradingHours {
1368 TradingHours::new(
1369 NaiveTime::from_hms_opt(10, 0, 0).unwrap(),
1370 NaiveTime::from_hms_opt(15, 30, 0).unwrap(),
1371 chrono_tz::Atlantic::Reykjavik,
1372 )
1373}
1374
1375fn xwar_rules() -> Vec<HolidayRule> {
1376 vec![
1380 fixed(1, 1, None),
1381 fixed_no_roll(1, 6, None),
1382 easter(1),
1383 fixed(5, 1, None),
1384 fixed_no_roll(5, 3, None),
1385 easter(60),
1386 fixed_no_roll(8, 15, None),
1387 fixed_no_roll(11, 1, None),
1388 fixed_no_roll(11, 11, None),
1389 fixed(12, 25, None),
1390 fixed(12, 26, None),
1391 ]
1392}
1393
1394fn xwar_hours() -> TradingHours {
1395 TradingHours::new(
1396 NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1397 NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
1398 chrono_tz::Europe::Warsaw,
1399 )
1400}
1401
1402fn xpra_rules() -> Vec<HolidayRule> {
1403 vec![
1407 fixed(1, 1, None),
1408 easter(-2),
1409 easter(1),
1410 fixed(5, 1, None),
1411 fixed_no_roll(5, 8, None),
1412 fixed_no_roll(7, 5, None),
1413 fixed_no_roll(7, 6, None),
1414 fixed_no_roll(9, 28, None),
1415 fixed_no_roll(10, 28, None),
1416 fixed_no_roll(11, 17, None),
1417 fixed_no_roll(12, 24, None),
1418 fixed(12, 25, None),
1419 fixed(12, 26, None),
1420 ]
1421}
1422
1423fn xpra_hours() -> TradingHours {
1424 TradingHours::new(
1425 NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1426 NaiveTime::from_hms_opt(16, 25, 0).unwrap(),
1427 chrono_tz::Europe::Prague,
1428 )
1429}
1430
1431fn xbud_rules() -> Vec<HolidayRule> {
1432 vec![
1436 fixed(1, 1, None),
1437 fixed_no_roll(3, 15, None),
1438 easter(-2),
1439 easter(1),
1440 fixed(5, 1, None),
1441 easter(50),
1442 fixed_no_roll(8, 20, None),
1443 fixed_no_roll(10, 23, None),
1444 fixed_no_roll(11, 1, None),
1445 fixed(12, 25, None),
1446 fixed(12, 26, None),
1447 ]
1448}
1449
1450fn xbud_hours() -> TradingHours {
1451 TradingHours::new(
1452 NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1453 NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
1454 chrono_tz::Europe::Budapest,
1455 )
1456}
1457
1458fn xwbo_rules() -> Vec<HolidayRule> {
1459 vec![
1463 fixed(1, 1, None),
1464 easter(-2),
1465 easter(1),
1466 fixed(5, 1, None),
1467 easter(39),
1468 easter(50),
1469 easter(60),
1470 fixed_no_roll(8, 15, None),
1471 fixed_no_roll(10, 26, None),
1472 fixed_no_roll(11, 1, None),
1473 fixed_no_roll(12, 8, None),
1474 fixed_no_roll(12, 24, None),
1475 fixed(12, 25, None),
1476 fixed(12, 26, None),
1477 ]
1478}
1479
1480fn xwbo_hours() -> TradingHours {
1481 TradingHours::new(
1482 NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1483 NaiveTime::from_hms_opt(17, 30, 0).unwrap(),
1484 chrono_tz::Europe::Vienna,
1485 )
1486}
1487
1488fn xdub_rules() -> Vec<HolidayRule> {
1489 vec![
1493 fixed(1, 1, None),
1494 fixed(3, 17, None),
1495 easter(-2),
1496 easter(1),
1497 nth(5, Weekday::Mon, 1),
1498 nth(6, Weekday::Mon, 1),
1499 nth(8, Weekday::Mon, 1),
1500 nth(10, Weekday::Mon, -1),
1501 fixed(12, 25, None),
1502 fixed(12, 26, None),
1503 ]
1504}
1505
1506fn xdub_hours() -> TradingHours {
1507 TradingHours::new(
1508 NaiveTime::from_hms_opt(8, 0, 0).unwrap(),
1509 NaiveTime::from_hms_opt(16, 28, 0).unwrap(),
1510 chrono_tz::Europe::Dublin,
1511 )
1512}
1513
1514fn xkrx_rules() -> Vec<HolidayRule> {
1517 let seollal: &'static [(i32, u32, u32)] = &[
1520 (2020, 1, 24),
1521 (2020, 1, 27),
1522 (2021, 2, 11),
1523 (2021, 2, 12),
1524 (2022, 1, 31),
1525 (2022, 2, 1),
1526 (2022, 2, 2),
1527 (2023, 1, 23),
1528 (2023, 1, 24),
1529 (2024, 2, 9),
1530 (2024, 2, 12),
1531 (2025, 1, 28),
1532 (2025, 1, 29),
1533 (2025, 1, 30),
1534 (2026, 2, 16),
1535 (2026, 2, 17),
1536 (2026, 2, 18),
1537 ];
1538 let chuseok: &'static [(i32, u32, u32)] = &[
1539 (2020, 9, 30),
1540 (2020, 10, 1),
1541 (2020, 10, 2),
1542 (2021, 9, 20),
1543 (2021, 9, 21),
1544 (2021, 9, 22),
1545 (2022, 9, 9),
1546 (2022, 9, 12),
1547 (2023, 9, 28),
1548 (2023, 9, 29),
1549 (2024, 9, 16),
1550 (2024, 9, 17),
1551 (2024, 9, 18),
1552 (2025, 10, 6),
1553 (2025, 10, 7),
1554 (2025, 10, 8),
1555 (2026, 9, 24),
1556 (2026, 9, 25),
1557 ];
1558 vec![
1559 fixed(1, 1, None),
1560 HolidayRule::Tabulated { table: seollal },
1561 fixed_no_roll(3, 1, None), fixed_no_roll(5, 5, None), fixed_no_roll(6, 6, None), fixed_no_roll(8, 15, None), HolidayRule::Tabulated { table: chuseok },
1566 fixed_no_roll(10, 3, None), fixed_no_roll(10, 9, None), fixed(12, 25, None),
1569 ]
1570}
1571
1572fn xkrx_hours() -> TradingHours {
1573 TradingHours::new(
1574 NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1575 NaiveTime::from_hms_opt(15, 30, 0).unwrap(),
1576 chrono_tz::Asia::Seoul,
1577 )
1578}
1579
1580fn xses_rules() -> Vec<HolidayRule> {
1581 let lny: &'static [(i32, u32, u32)] = &[
1585 (2020, 1, 24),
1586 (2021, 2, 12),
1587 (2022, 2, 1),
1588 (2023, 1, 23),
1589 (2024, 2, 12),
1590 (2025, 1, 29),
1591 (2026, 2, 17),
1592 ];
1593 let lny2: &'static [(i32, u32, u32)] = &[
1594 (2020, 1, 27),
1595 (2021, 2, 15),
1596 (2022, 2, 2),
1597 (2023, 1, 24),
1598 (2024, 2, 13),
1599 (2025, 1, 30),
1600 (2026, 2, 18),
1601 ];
1602 vec![
1603 fixed(1, 1, None),
1604 HolidayRule::Tabulated { table: lny },
1605 HolidayRule::Tabulated { table: lny2 },
1606 easter(-2),
1607 fixed(5, 1, None),
1608 fixed(8, 9, None),
1609 fixed(12, 25, None),
1610 ]
1611}
1612
1613fn xses_hours() -> TradingHours {
1614 TradingHours::new(
1615 NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1616 NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
1617 chrono_tz::Asia::Singapore,
1618 )
1619}
1620
1621fn xtai_rules() -> Vec<HolidayRule> {
1622 let lny: &'static [(i32, u32, u32)] = &[
1626 (2020, 1, 23),
1627 (2021, 2, 8),
1628 (2022, 1, 27),
1629 (2023, 1, 19),
1630 (2024, 2, 5),
1631 (2025, 1, 23),
1632 (2026, 2, 13),
1633 ];
1634 vec![
1635 fixed(1, 1, None),
1636 HolidayRule::Tabulated { table: lny },
1637 fixed_no_roll(2, 28, None), fixed_no_roll(4, 4, None), fixed_no_roll(4, 5, None), fixed(5, 1, None),
1641 fixed_no_roll(10, 10, None), ]
1643}
1644
1645fn xtai_hours() -> TradingHours {
1646 TradingHours::new(
1647 NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1648 NaiveTime::from_hms_opt(13, 30, 0).unwrap(),
1649 chrono_tz::Asia::Taipei,
1650 )
1651}
1652
1653fn xbkk_rules() -> Vec<HolidayRule> {
1654 vec![
1659 fixed(1, 1, None),
1660 fixed(4, 6, None),
1661 fixed_no_roll(4, 13, None),
1662 fixed_no_roll(4, 14, None),
1663 fixed_no_roll(4, 15, None),
1664 fixed(5, 1, None),
1665 fixed_no_roll(5, 4, None),
1666 fixed_no_roll(8, 12, None),
1667 fixed(10, 13, None),
1668 fixed(10, 23, None),
1669 fixed(12, 5, None),
1670 fixed(12, 10, None),
1671 fixed_no_roll(12, 31, None),
1672 ]
1673}
1674
1675fn xbkk_hours() -> TradingHours {
1676 TradingHours::new(
1677 NaiveTime::from_hms_opt(10, 0, 0).unwrap(),
1678 NaiveTime::from_hms_opt(16, 30, 0).unwrap(),
1679 chrono_tz::Asia::Bangkok,
1680 )
1681}
1682
1683fn xkls_rules() -> Vec<HolidayRule> {
1684 let lny: &'static [(i32, u32, u32)] = &[
1688 (2020, 1, 27),
1689 (2021, 2, 12),
1690 (2022, 2, 1),
1691 (2023, 1, 23),
1692 (2024, 2, 12),
1693 (2025, 1, 29),
1694 (2026, 2, 17),
1695 ];
1696 vec![
1697 fixed(1, 1, None),
1698 HolidayRule::Tabulated { table: lny },
1699 fixed(5, 1, None),
1700 nth(6, Weekday::Mon, 1),
1701 fixed(8, 31, None),
1702 fixed(9, 16, None),
1703 fixed(12, 25, None),
1704 ]
1705}
1706
1707fn xkls_hours() -> TradingHours {
1708 TradingHours::new(
1709 NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1710 NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
1711 chrono_tz::Asia::Kuala_Lumpur,
1712 )
1713}
1714
1715fn xidx_rules() -> Vec<HolidayRule> {
1716 let lny: &'static [(i32, u32, u32)] = &[
1719 (2020, 1, 27),
1720 (2021, 2, 12),
1721 (2022, 2, 1),
1722 (2023, 1, 23),
1723 (2024, 2, 8),
1724 (2025, 1, 29),
1725 (2026, 2, 17),
1726 ];
1727 vec![
1728 fixed(1, 1, None),
1729 HolidayRule::Tabulated { table: lny },
1730 fixed(5, 1, None),
1731 fixed(6, 1, None),
1732 fixed(8, 17, None),
1733 fixed(12, 25, None),
1734 ]
1735}
1736
1737fn xidx_hours() -> TradingHours {
1738 TradingHours::new(
1739 NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1740 NaiveTime::from_hms_opt(15, 50, 0).unwrap(),
1741 chrono_tz::Asia::Jakarta,
1742 )
1743}
1744
1745fn xphs_rules() -> Vec<HolidayRule> {
1746 vec![
1751 fixed(1, 1, None),
1752 easter(-3),
1753 easter(-2),
1754 fixed(4, 9, None),
1755 fixed(5, 1, None),
1756 fixed(6, 12, None),
1757 fixed(8, 21, None),
1758 nth(8, Weekday::Mon, -1),
1759 fixed_no_roll(11, 1, None),
1760 fixed(11, 30, None),
1761 fixed(12, 25, None),
1762 fixed(12, 30, None),
1763 fixed_no_roll(12, 31, None),
1764 ]
1765}
1766
1767fn xphs_hours() -> TradingHours {
1768 TradingHours::new(
1769 NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
1770 NaiveTime::from_hms_opt(15, 0, 0).unwrap(),
1771 chrono_tz::Asia::Manila,
1772 )
1773}
1774
1775fn xnze_rules() -> Vec<HolidayRule> {
1776 vec![
1781 fixed(1, 1, None),
1782 fixed(1, 2, None),
1783 fixed(2, 6, None),
1784 easter(-2),
1785 easter(1),
1786 fixed(4, 25, None),
1787 nth(6, Weekday::Mon, 1),
1788 nth(10, Weekday::Mon, 4),
1789 fixed(12, 25, None),
1790 fixed(12, 26, None),
1791 ]
1792}
1793
1794fn xnze_hours() -> TradingHours {
1795 TradingHours::new(
1796 NaiveTime::from_hms_opt(10, 0, 0).unwrap(),
1797 NaiveTime::from_hms_opt(16, 45, 0).unwrap(),
1798 chrono_tz::Pacific::Auckland,
1799 )
1800}
1801
1802fn xjse_rules() -> Vec<HolidayRule> {
1805 vec![
1810 fixed(1, 1, None),
1811 fixed(3, 21, None),
1812 easter(-2),
1813 easter(1),
1814 fixed(4, 27, None),
1815 fixed(5, 1, None),
1816 fixed(6, 16, None),
1817 fixed(8, 9, None),
1818 fixed(9, 24, None),
1819 fixed(12, 16, None),
1820 fixed(12, 25, None),
1821 fixed(12, 26, None),
1822 ]
1823}
1824
1825fn xjse_hours() -> TradingHours {
1826 TradingHours::new(
1827 NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1828 NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
1829 chrono_tz::Africa::Johannesburg,
1830 )
1831}
1832
1833const MIDEAST_WEEKMASK: [bool; 7] = [true, true, true, true, false, false, true];
1835
1836fn xsau_rules() -> Vec<HolidayRule> {
1837 let eid_fitr: &'static [(i32, u32, u32)] = &[
1840 (2020, 5, 24),
1841 (2021, 5, 13),
1842 (2022, 5, 2),
1843 (2023, 4, 21),
1844 (2024, 4, 10),
1845 (2025, 3, 30),
1846 (2026, 3, 20),
1847 ];
1848 let eid_adha: &'static [(i32, u32, u32)] = &[
1849 (2020, 7, 31),
1850 (2021, 7, 20),
1851 (2022, 7, 9),
1852 (2023, 6, 28),
1853 (2024, 6, 16),
1854 (2025, 6, 6),
1855 (2026, 5, 27),
1856 ];
1857 vec![
1858 fixed_no_roll(2, 22, Some(2022)),
1859 fixed_no_roll(9, 23, None),
1860 HolidayRule::Tabulated { table: eid_fitr },
1861 HolidayRule::Tabulated { table: eid_adha },
1862 ]
1863}
1864
1865fn xsau_hours() -> TradingHours {
1866 TradingHours::new(
1867 NaiveTime::from_hms_opt(10, 0, 0).unwrap(),
1868 NaiveTime::from_hms_opt(15, 0, 0).unwrap(),
1869 chrono_tz::Asia::Riyadh,
1870 )
1871}
1872
1873fn xist_rules() -> Vec<HolidayRule> {
1874 let eid_fitr: &'static [(i32, u32, u32)] = &[
1878 (2020, 5, 24),
1879 (2021, 5, 13),
1880 (2022, 5, 2),
1881 (2023, 4, 21),
1882 (2024, 4, 10),
1883 (2025, 3, 30),
1884 (2026, 3, 20),
1885 ];
1886 let eid_adha: &'static [(i32, u32, u32)] = &[
1887 (2020, 7, 31),
1888 (2021, 7, 20),
1889 (2022, 7, 9),
1890 (2023, 6, 28),
1891 (2024, 6, 16),
1892 (2025, 6, 6),
1893 (2026, 5, 27),
1894 ];
1895 vec![
1896 fixed(1, 1, None),
1897 fixed(4, 23, None),
1898 fixed(5, 1, None),
1899 fixed(5, 19, None),
1900 fixed(7, 15, None),
1901 fixed(8, 30, None),
1902 fixed(10, 29, None),
1903 HolidayRule::Tabulated { table: eid_fitr },
1904 HolidayRule::Tabulated { table: eid_adha },
1905 ]
1906}
1907
1908fn xist_hours() -> TradingHours {
1909 TradingHours::new(
1910 NaiveTime::from_hms_opt(10, 0, 0).unwrap(),
1911 NaiveTime::from_hms_opt(18, 0, 0).unwrap(),
1912 chrono_tz::Europe::Istanbul,
1913 )
1914}
1915
1916const TASE_WEEKMASK: [bool; 7] = [true, true, true, true, false, false, true];
1918
1919fn xtae_rules() -> Vec<HolidayRule> {
1920 let purim: &'static [(i32, u32, u32)] = &[
1924 (2020, 3, 10),
1925 (2021, 2, 26),
1926 (2022, 3, 17),
1927 (2023, 3, 7),
1928 (2024, 3, 24),
1929 (2025, 3, 14),
1930 (2026, 3, 3),
1931 ];
1932 let passover_eve: &'static [(i32, u32, u32)] = &[
1933 (2020, 4, 8),
1934 (2021, 3, 27),
1935 (2022, 4, 15),
1936 (2023, 4, 5),
1937 (2024, 4, 22),
1938 (2025, 4, 12),
1939 (2026, 4, 1),
1940 ];
1941 let shavuot: &'static [(i32, u32, u32)] = &[
1942 (2020, 5, 29),
1943 (2021, 5, 17),
1944 (2022, 6, 5),
1945 (2023, 5, 26),
1946 (2024, 6, 12),
1947 (2025, 6, 2),
1948 (2026, 5, 22),
1949 ];
1950 let rosh: &'static [(i32, u32, u32)] = &[
1951 (2020, 9, 19),
1952 (2021, 9, 7),
1953 (2022, 9, 26),
1954 (2023, 9, 16),
1955 (2024, 10, 3),
1956 (2025, 9, 23),
1957 (2026, 9, 12),
1958 ];
1959 let yom_kippur: &'static [(i32, u32, u32)] = &[
1960 (2020, 9, 28),
1961 (2021, 9, 16),
1962 (2022, 10, 5),
1963 (2023, 9, 25),
1964 (2024, 10, 12),
1965 (2025, 10, 2),
1966 (2026, 9, 21),
1967 ];
1968 let sukkot: &'static [(i32, u32, u32)] = &[
1969 (2020, 10, 3),
1970 (2021, 9, 21),
1971 (2022, 10, 10),
1972 (2023, 9, 30),
1973 (2024, 10, 17),
1974 (2025, 10, 7),
1975 (2026, 9, 26),
1976 ];
1977 let independence: &'static [(i32, u32, u32)] = &[
1978 (2020, 4, 29),
1979 (2021, 4, 15),
1980 (2022, 5, 5),
1981 (2023, 4, 26),
1982 (2024, 5, 14),
1983 (2025, 5, 1),
1984 (2026, 4, 22),
1985 ];
1986 vec![
1987 HolidayRule::Tabulated { table: purim },
1988 HolidayRule::Tabulated {
1989 table: passover_eve,
1990 },
1991 HolidayRule::Tabulated { table: shavuot },
1992 HolidayRule::Tabulated {
1993 table: independence,
1994 },
1995 HolidayRule::Tabulated { table: rosh },
1996 HolidayRule::Tabulated { table: yom_kippur },
1997 HolidayRule::Tabulated { table: sukkot },
1998 ]
1999}
2000
2001fn xtae_hours() -> TradingHours {
2002 TradingHours::new(
2003 NaiveTime::from_hms_opt(9, 59, 0).unwrap(),
2004 NaiveTime::from_hms_opt(17, 14, 0).unwrap(),
2005 chrono_tz::Asia::Jerusalem,
2006 )
2007}
2008
2009fn xdfm_rules() -> Vec<HolidayRule> {
2010 let eid_fitr: &'static [(i32, u32, u32)] = &[
2013 (2020, 5, 24),
2014 (2021, 5, 13),
2015 (2022, 5, 2),
2016 (2023, 4, 21),
2017 (2024, 4, 10),
2018 (2025, 3, 30),
2019 (2026, 3, 20),
2020 ];
2021 let eid_adha: &'static [(i32, u32, u32)] = &[
2022 (2020, 7, 31),
2023 (2021, 7, 20),
2024 (2022, 7, 9),
2025 (2023, 6, 28),
2026 (2024, 6, 16),
2027 (2025, 6, 6),
2028 (2026, 5, 27),
2029 ];
2030 vec![
2031 fixed(1, 1, None),
2032 fixed(11, 30, None),
2033 fixed(12, 2, None),
2034 fixed(12, 3, None),
2035 HolidayRule::Tabulated { table: eid_fitr },
2036 HolidayRule::Tabulated { table: eid_adha },
2037 ]
2038}
2039
2040fn xdfm_hours() -> TradingHours {
2041 TradingHours::new(
2042 NaiveTime::from_hms_opt(10, 0, 0).unwrap(),
2043 NaiveTime::from_hms_opt(15, 0, 0).unwrap(),
2044 chrono_tz::Asia::Dubai,
2045 )
2046}
2047
2048fn bvmf_rules() -> Vec<HolidayRule> {
2051 vec![
2056 fixed(1, 1, None),
2057 easter(-48),
2058 easter(-47),
2059 easter(-2),
2060 fixed(4, 21, None),
2061 fixed(5, 1, None),
2062 easter(60),
2063 fixed(9, 7, None),
2064 fixed(10, 12, None),
2065 fixed(11, 2, None),
2066 fixed(11, 15, None),
2067 fixed(11, 20, Some(2024)),
2068 fixed_no_roll(12, 24, None),
2069 fixed(12, 25, None),
2070 fixed_no_roll(12, 31, None),
2071 ]
2072}
2073
2074fn bvmf_hours() -> TradingHours {
2075 TradingHours::new(
2076 NaiveTime::from_hms_opt(10, 0, 0).unwrap(),
2077 NaiveTime::from_hms_opt(17, 30, 0).unwrap(),
2078 chrono_tz::America::Sao_Paulo,
2079 )
2080}
2081
2082fn xmex_rules() -> Vec<HolidayRule> {
2083 vec![
2087 fixed(1, 1, None),
2088 nth(2, Weekday::Mon, 1),
2089 nth(3, Weekday::Mon, 3),
2090 easter(-3),
2091 easter(-2),
2092 fixed(5, 1, None),
2093 fixed(9, 16, None),
2094 nth(11, Weekday::Mon, 3),
2095 fixed(12, 25, None),
2096 ]
2097}
2098
2099fn xmex_hours() -> TradingHours {
2100 TradingHours::new(
2101 NaiveTime::from_hms_opt(8, 30, 0).unwrap(),
2102 NaiveTime::from_hms_opt(15, 0, 0).unwrap(),
2103 chrono_tz::America::Mexico_City,
2104 )
2105}
2106
2107fn xbue_rules() -> Vec<HolidayRule> {
2108 vec![
2113 fixed(1, 1, None),
2114 easter(-48),
2115 easter(-47),
2116 fixed(3, 24, None),
2117 fixed(4, 2, None),
2118 easter(-2),
2119 fixed(5, 1, None),
2120 fixed(5, 25, None),
2121 fixed(6, 20, None),
2122 fixed(7, 9, None),
2123 nth(8, Weekday::Mon, 3),
2124 fixed(10, 12, None),
2125 fixed(11, 20, None),
2126 fixed(12, 8, None),
2127 fixed(12, 25, None),
2128 ]
2129}
2130
2131fn xbue_hours() -> TradingHours {
2132 TradingHours::new(
2133 NaiveTime::from_hms_opt(11, 0, 0).unwrap(),
2134 NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
2135 chrono_tz::America::Argentina::Buenos_Aires,
2136 )
2137}
2138
2139fn xsgo_rules() -> Vec<HolidayRule> {
2140 vec![
2145 fixed(1, 1, None),
2146 easter(-2),
2147 easter(-1),
2148 fixed(5, 1, None),
2149 fixed(5, 21, None),
2150 fixed(6, 29, None),
2151 fixed(7, 16, None),
2152 fixed(8, 15, None),
2153 fixed(9, 18, None),
2154 fixed(9, 19, None),
2155 fixed(10, 12, None),
2156 fixed(10, 31, None),
2157 fixed(11, 1, None),
2158 fixed(12, 8, None),
2159 fixed(12, 25, None),
2160 ]
2161}
2162
2163fn xsgo_hours() -> TradingHours {
2164 TradingHours::new(
2165 NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
2166 NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
2167 chrono_tz::America::Santiago,
2168 )
2169}
2170
2171fn xlim_rules() -> Vec<HolidayRule> {
2172 vec![
2176 fixed(1, 1, None),
2177 easter(-3),
2178 easter(-2),
2179 fixed(5, 1, None),
2180 fixed(6, 29, None),
2181 fixed(7, 28, None),
2182 fixed(7, 29, None),
2183 fixed(8, 30, None),
2184 fixed(10, 8, None),
2185 fixed(11, 1, None),
2186 fixed(12, 8, None),
2187 fixed(12, 25, None),
2188 ]
2189}
2190
2191fn xlim_hours() -> TradingHours {
2192 TradingHours::new(
2193 NaiveTime::from_hms_opt(8, 30, 0).unwrap(),
2194 NaiveTime::from_hms_opt(15, 0, 0).unwrap(),
2195 chrono_tz::America::Lima,
2196 )
2197}
2198
2199fn xbog_rules() -> Vec<HolidayRule> {
2200 vec![
2206 fixed(1, 1, None),
2207 fixed(1, 6, None),
2208 fixed(3, 19, None),
2209 easter(-3),
2210 easter(-2),
2211 fixed(5, 1, None),
2212 easter(39),
2213 easter(60),
2214 easter(68),
2215 fixed(7, 20, None),
2216 fixed(8, 7, None),
2217 fixed(8, 15, None),
2218 fixed(10, 12, None),
2219 fixed(11, 1, None),
2220 fixed(11, 11, None),
2221 fixed(12, 8, None),
2222 fixed(12, 25, None),
2223 ]
2224}
2225
2226fn xbog_hours() -> TradingHours {
2227 TradingHours::new(
2228 NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
2229 NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
2230 chrono_tz::America::Bogota,
2231 )
2232}
2233
2234#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2238enum Family {
2239 UsEquity,
2240 UsOptions,
2241 UsBondSifma,
2242 UsFuturesCme,
2243 UsFuturesCmeEnergy,
2244 UsFuturesCbotGrains,
2245 UsFuturesCmeLivestock,
2246 UsFuturesCmeLumber,
2247 UsFuturesIce,
2248 UsFuturesCfe,
2249 Forex24x5,
2250 Crypto24x7,
2251 Lse,
2252 Tse,
2253 Hkex,
2254 Sse,
2255 Xetra,
2256 EuronextParis,
2257 EuronextAms,
2258 EuronextBru,
2259 EuronextLis,
2260 EuronextDub,
2261 Tsx,
2262 Asx,
2263 Nse,
2264 Xmil,
2265 Xmad,
2266 Xswx,
2267 Xosl,
2268 Xsto,
2269 Xhel,
2270 Xcse,
2271 Xice,
2272 Xwar,
2273 Xpra,
2274 Xbud,
2275 Xwbo,
2276 Xkrx,
2277 Xses,
2278 Xtai,
2279 Xbkk,
2280 Xkls,
2281 Xidx,
2282 Xphs,
2283 Xnze,
2284 Xjse,
2285 Xsau,
2286 Xist,
2287 Xtae,
2288 Xdfm,
2289 Bvmf,
2290 Xmex,
2291 Xbue,
2292 Xsgo,
2293 Xlim,
2294 Xbog,
2295}
2296
2297fn family_for_mic(mic: &str) -> Option<Family> {
2298 use Family::*;
2299 let m = match mic {
2300 "XNYS" | "NYSD" | "XCIS" | "CISD" | "XCHI" | "ARCX" | "ARCD" | "ARCO"
2302 | "XASE" | "AMXO" | "XNAS" | "XNGS" | "XNCM" | "XNMS" | "NASD" | "XNDQ"
2303 | "XBOS" | "BOSD" | "XBXO" | "XPHL" | "XPSX" | "PSXD" | "XPHO" | "XPBT"
2304 | "XPOR" | "XNFI" | "EDGA" | "EDGD" | "EDGX" | "EDDP" | "EDGO" | "BATS"
2305 | "BZXD" | "BATO" | "BATY" | "BYXD" | "MEMX" | "MEMD" | "IEXG" | "LTSE"
2306 | "MIHI" | "MPRL" | "EPRL" | "EPRD" | "XMIO" | "EMLD"
2307 | "OTCM" | "CAVE" | "OTCB" | "OTCQ" | "PINL" | "PINI" | "PINX" | "PSGM"
2309 | "PINC" | "FINR" | "FINN" | "FINC" | "FINY" | "XADF" | "FINO" | "OOTC"
2310 | "XXXX" | "PYPR" | "SIMU" => UsEquity,
2312 "XISE" | "GMNI" | "MCRY" | "XCBO" | "C2OX" | "MXOP" | "OPRA" => UsOptions,
2314 "XCME" | "FCME" | "GLBX" | "XCBT" | "FCBT" | "XKBT"
2319 | "SR3" | "ES" | "NQ" | "RTY"
2321 | "CME_DAIRY" | "GLOBEX_DAIRY" => UsFuturesCme,
2322 "LE" | "GF" | "HE" | "CME_LIVESTOCK" | "GLOBEX_LIVESTOCK" => {
2324 UsFuturesCmeLivestock
2325 }
2326 "LBR" | "LS" | "CME_LUMBER" | "GLOBEX_LUMBER" => UsFuturesCmeLumber,
2328 "XNYM" | "NYMEX_ENERGY" | "COMEX_METALS"
2330 | "CL" | "MCL" | "QM" | "GC" | "MGC" | "QO"
2332 | "CME_ENERGY" | "GLOBEX_ENERGY"
2333 | "CME_METALS" | "GLOBEX_METALS" => UsFuturesCmeEnergy,
2334 "CBOT_GRAINS" | "CME_GRAINS" | "GLOBEX_GRAINS"
2337 | "ZC" | "ZW" | "ZS" | "ZL" | "ZM" | "ZO" | "KE" | "HRS"
2339 | "CBOT_OILSEEDS" | "CBOT_WHEAT" | "CBOT_CORN" | "CBOT_SOYBEANS" => {
2340 UsFuturesCbotGrains
2341 }
2342 "CFE" => UsFuturesCfe,
2344 "ICE_US" => UsFuturesIce,
2345 "SIFMA_US" => UsBondSifma,
2346 "FOREX" => Forex24x5,
2347 "CRYPTO" => Crypto24x7,
2348 "XTSE" | "XDRK" | "VDRK" | "XTSX" | "XTNX" | "XATS" | "XATX" | "ADRK"
2350 | "XMOD" | "XMOC" | "NEOE" | "NEOD" | "NEON" | "NEOC" | "XCNQ" | "PURE"
2351 | "CSE2" => Tsx,
2352 "XLON" => Lse,
2354 "XTKS" => Tse,
2355 "XHKG" => Hkex,
2356 "XSHG" => Sse,
2357 "21XX" | "XEUR" | "XFRA" => Xetra,
2358 "XPAR" => EuronextParis,
2359 "XAMS" => EuronextAms,
2360 "XBRU" => EuronextBru,
2361 "XLIS" => EuronextLis,
2362 "XDUB" => EuronextDub,
2363 "XMIL" => Xmil,
2364 "XMAD" => Xmad,
2365 "XSWX" => Xswx,
2366 "XOSL" => Xosl,
2367 "XSTO" => Xsto,
2368 "XHEL" => Xhel,
2369 "XCSE" => Xcse,
2370 "XICE" => Xice,
2371 "XWAR" => Xwar,
2372 "XPRA" => Xpra,
2373 "XBUD" => Xbud,
2374 "XWBO" => Xwbo,
2375 "XASX" => Asx,
2376 "XBOM" | "XNSE" => Nse,
2377 "XKRX" => Xkrx,
2378 "XSES" => Xses,
2379 "XTAI" => Xtai,
2380 "XBKK" => Xbkk,
2381 "XKLS" => Xkls,
2382 "XIDX" => Xidx,
2383 "XPHS" => Xphs,
2384 "XNZE" => Xnze,
2385 "XJSE" => Xjse,
2386 "XSAU" => Xsau,
2387 "XIST" => Xist,
2388 "XTAE" => Xtae,
2389 "XDFM" | "XADS" => Xdfm,
2390 "BVMF" => Bvmf,
2391 "XMEX" => Xmex,
2392 "XBUE" => Xbue,
2393 "XSGO" => Xsgo,
2394 "XLIM" => Xlim,
2395 "XBOG" => Xbog,
2396 _ => return None,
2397 };
2398 Some(m)
2399}
2400
2401fn build_family(name: &str, fam: Family) -> Calendar {
2402 use Family::*;
2403 match fam {
2404 UsEquity => Calendar::with_type(
2405 name,
2406 market_type("Equities"),
2407 STANDARD_WEEKMASK,
2408 nyse_rules(),
2409 Some(nyse_trading_hours()),
2410 )
2411 .with_early_closes(nyse_early_closes()),
2412 UsOptions => Calendar::with_type(
2413 name,
2414 market_type("Options"),
2415 STANDARD_WEEKMASK,
2416 nyse_rules(),
2417 Some(options_trading_hours()),
2418 )
2419 .with_early_closes(nyse_early_closes()),
2420 UsBondSifma => Calendar::with_type(
2421 name,
2422 market_type("FixedIncome"),
2423 STANDARD_WEEKMASK,
2424 sifma_us_rules(),
2425 Some(sifma_us_hours()),
2426 ),
2427 UsFuturesCme => Calendar::with_type(
2428 name,
2429 market_type("Futures"),
2430 STANDARD_WEEKMASK,
2431 cme_globex_rules(),
2432 Some(cme_globex_overnight_hours()),
2433 ),
2434 UsFuturesCmeEnergy => Calendar::with_type(
2435 name,
2436 market_type("Futures"),
2437 STANDARD_WEEKMASK,
2438 cme_globex_rules(),
2439 Some(cme_globex_energy_hours()),
2440 ),
2441 UsFuturesCbotGrains => Calendar::with_type(
2442 name,
2443 market_type("Futures"),
2444 STANDARD_WEEKMASK,
2445 cme_globex_rules(),
2446 Some(cbot_grain_futures_hours()),
2447 ),
2448 UsFuturesCmeLivestock => Calendar::with_type(
2449 name,
2450 market_type("Futures"),
2451 STANDARD_WEEKMASK,
2452 cme_globex_rules(),
2453 Some(cme_livestock_hours()),
2454 ),
2455 UsFuturesCmeLumber => Calendar::with_type(
2456 name,
2457 market_type("Futures"),
2458 STANDARD_WEEKMASK,
2459 cme_globex_rules(),
2460 Some(cme_lumber_hours()),
2461 ),
2462 UsFuturesIce => Calendar::with_type(
2463 name,
2464 market_type("Futures"),
2465 STANDARD_WEEKMASK,
2466 ice_us_rules(),
2467 Some(ice_us_hours()),
2468 ),
2469 UsFuturesCfe => Calendar::with_type(
2470 name,
2471 market_type("Futures"),
2472 STANDARD_WEEKMASK,
2473 cfe_rules(),
2474 Some(cfe_trading_hours()),
2475 ),
2476 Forex24x5 => Calendar::with_type(
2477 name,
2478 market_type("ForeignExchange"),
2479 STANDARD_WEEKMASK,
2480 forex_rules(),
2481 Some(TradingHours::forex_24x5()),
2482 ),
2483 Crypto24x7 => Calendar::with_type(
2484 name,
2485 market_type("DigitalAssets"),
2486 CRYPTO_WEEKMASK,
2487 crypto_rules(),
2488 Some(TradingHours::crypto_24x7()),
2489 ),
2490 Lse => Calendar::with_type(
2491 name,
2492 market_type("Equities"),
2493 STANDARD_WEEKMASK,
2494 lse_rules(),
2495 Some(lse_trading_hours()),
2496 ),
2497 Tse => Calendar::with_type(
2498 name,
2499 market_type("Equities"),
2500 STANDARD_WEEKMASK,
2501 tse_rules(),
2502 Some(tse_trading_hours()),
2503 )
2504 .with_schedules(tse_schedules()),
2505 Hkex => Calendar::with_type(
2506 name,
2507 market_type("Equities"),
2508 STANDARD_WEEKMASK,
2509 hkex_rules(),
2510 Some(hkex_trading_hours()),
2511 ),
2512 Sse => Calendar::with_type(
2513 name,
2514 market_type("Equities"),
2515 STANDARD_WEEKMASK,
2516 sse_rules(),
2517 Some(sse_trading_hours()),
2518 ),
2519 Xetra => Calendar::with_type(
2520 name,
2521 market_type("Equities"),
2522 STANDARD_WEEKMASK,
2523 xetra_rules(),
2524 Some(xetra_trading_hours()),
2525 ),
2526 EuronextParis => Calendar::with_type(
2527 name,
2528 market_type("Equities"),
2529 STANDARD_WEEKMASK,
2530 euronext_paris_rules(),
2531 Some(euronext_paris_trading_hours()),
2532 ),
2533 EuronextAms => Calendar::with_type(
2534 name,
2535 market_type("Equities"),
2536 STANDARD_WEEKMASK,
2537 xams_rules(),
2538 Some(euronext_hours(chrono_tz::Europe::Amsterdam)),
2539 ),
2540 EuronextBru => Calendar::with_type(
2541 name,
2542 market_type("Equities"),
2543 STANDARD_WEEKMASK,
2544 xbru_rules(),
2545 Some(euronext_hours(chrono_tz::Europe::Brussels)),
2546 ),
2547 EuronextLis => Calendar::with_type(
2548 name,
2549 market_type("Equities"),
2550 STANDARD_WEEKMASK,
2551 xlis_rules(),
2552 Some(euronext_hours(chrono_tz::Europe::Lisbon)),
2553 ),
2554 EuronextDub => Calendar::with_type(
2555 name,
2556 market_type("Equities"),
2557 STANDARD_WEEKMASK,
2558 xdub_rules(),
2559 Some(xdub_hours()),
2560 ),
2561 Tsx => Calendar::with_type(
2562 name,
2563 market_type("Equities"),
2564 STANDARD_WEEKMASK,
2565 tsx_rules(),
2566 Some(tsx_trading_hours()),
2567 ),
2568 Asx => Calendar::with_type(
2569 name,
2570 market_type("Equities"),
2571 STANDARD_WEEKMASK,
2572 asx_rules(),
2573 Some(asx_trading_hours()),
2574 ),
2575 Nse => Calendar::with_type(
2576 name,
2577 market_type("Equities"),
2578 STANDARD_WEEKMASK,
2579 nse_rules(),
2580 Some(nse_trading_hours()),
2581 ),
2582 Xmil => Calendar::with_type(
2583 name,
2584 market_type("Equities"),
2585 STANDARD_WEEKMASK,
2586 xmil_rules(),
2587 Some(xmil_hours()),
2588 ),
2589 Xmad => Calendar::with_type(
2590 name,
2591 market_type("Equities"),
2592 STANDARD_WEEKMASK,
2593 xmad_rules(),
2594 Some(xmad_hours()),
2595 ),
2596 Xswx => Calendar::with_type(
2597 name,
2598 market_type("Equities"),
2599 STANDARD_WEEKMASK,
2600 xswx_rules(),
2601 Some(xswx_hours()),
2602 ),
2603 Xosl => Calendar::with_type(
2604 name,
2605 market_type("Equities"),
2606 STANDARD_WEEKMASK,
2607 xosl_rules(),
2608 Some(xosl_hours()),
2609 ),
2610 Xsto => Calendar::with_type(
2611 name,
2612 market_type("Equities"),
2613 STANDARD_WEEKMASK,
2614 xsto_rules(),
2615 Some(xsto_hours()),
2616 ),
2617 Xhel => Calendar::with_type(
2618 name,
2619 market_type("Equities"),
2620 STANDARD_WEEKMASK,
2621 xhel_rules(),
2622 Some(xhel_hours()),
2623 ),
2624 Xcse => Calendar::with_type(
2625 name,
2626 market_type("Equities"),
2627 STANDARD_WEEKMASK,
2628 xcse_rules(),
2629 Some(xcse_hours()),
2630 ),
2631 Xice => Calendar::with_type(
2632 name,
2633 market_type("Equities"),
2634 STANDARD_WEEKMASK,
2635 xice_rules(),
2636 Some(xice_hours()),
2637 ),
2638 Xwar => Calendar::with_type(
2639 name,
2640 market_type("Equities"),
2641 STANDARD_WEEKMASK,
2642 xwar_rules(),
2643 Some(xwar_hours()),
2644 ),
2645 Xpra => Calendar::with_type(
2646 name,
2647 market_type("Equities"),
2648 STANDARD_WEEKMASK,
2649 xpra_rules(),
2650 Some(xpra_hours()),
2651 ),
2652 Xbud => Calendar::with_type(
2653 name,
2654 market_type("Equities"),
2655 STANDARD_WEEKMASK,
2656 xbud_rules(),
2657 Some(xbud_hours()),
2658 ),
2659 Xwbo => Calendar::with_type(
2660 name,
2661 market_type("Equities"),
2662 STANDARD_WEEKMASK,
2663 xwbo_rules(),
2664 Some(xwbo_hours()),
2665 ),
2666 Xkrx => Calendar::with_type(
2667 name,
2668 market_type("Equities"),
2669 STANDARD_WEEKMASK,
2670 xkrx_rules(),
2671 Some(xkrx_hours()),
2672 ),
2673 Xses => Calendar::with_type(
2674 name,
2675 market_type("Equities"),
2676 STANDARD_WEEKMASK,
2677 xses_rules(),
2678 Some(xses_hours()),
2679 ),
2680 Xtai => Calendar::with_type(
2681 name,
2682 market_type("Equities"),
2683 STANDARD_WEEKMASK,
2684 xtai_rules(),
2685 Some(xtai_hours()),
2686 ),
2687 Xbkk => Calendar::with_type(
2688 name,
2689 market_type("Equities"),
2690 STANDARD_WEEKMASK,
2691 xbkk_rules(),
2692 Some(xbkk_hours()),
2693 ),
2694 Xkls => Calendar::with_type(
2695 name,
2696 market_type("Equities"),
2697 STANDARD_WEEKMASK,
2698 xkls_rules(),
2699 Some(xkls_hours()),
2700 ),
2701 Xidx => Calendar::with_type(
2702 name,
2703 market_type("Equities"),
2704 STANDARD_WEEKMASK,
2705 xidx_rules(),
2706 Some(xidx_hours()),
2707 ),
2708 Xphs => Calendar::with_type(
2709 name,
2710 market_type("Equities"),
2711 STANDARD_WEEKMASK,
2712 xphs_rules(),
2713 Some(xphs_hours()),
2714 ),
2715 Xnze => Calendar::with_type(
2716 name,
2717 market_type("Equities"),
2718 STANDARD_WEEKMASK,
2719 xnze_rules(),
2720 Some(xnze_hours()),
2721 ),
2722 Xjse => Calendar::with_type(
2723 name,
2724 market_type("Equities"),
2725 STANDARD_WEEKMASK,
2726 xjse_rules(),
2727 Some(xjse_hours()),
2728 ),
2729 Xsau => Calendar::with_type(
2730 name,
2731 market_type("Equities"),
2732 MIDEAST_WEEKMASK,
2733 xsau_rules(),
2734 Some(xsau_hours()),
2735 ),
2736 Xist => Calendar::with_type(
2737 name,
2738 market_type("Equities"),
2739 STANDARD_WEEKMASK,
2740 xist_rules(),
2741 Some(xist_hours()),
2742 ),
2743 Xtae => Calendar::with_type(
2744 name,
2745 market_type("Equities"),
2746 TASE_WEEKMASK,
2747 xtae_rules(),
2748 Some(xtae_hours()),
2749 ),
2750 Xdfm => Calendar::with_type(
2751 name,
2752 market_type("Equities"),
2753 STANDARD_WEEKMASK,
2754 xdfm_rules(),
2755 Some(xdfm_hours()),
2756 ),
2757 Bvmf => Calendar::with_type(
2758 name,
2759 market_type("Equities"),
2760 STANDARD_WEEKMASK,
2761 bvmf_rules(),
2762 Some(bvmf_hours()),
2763 ),
2764 Xmex => Calendar::with_type(
2765 name,
2766 market_type("Equities"),
2767 STANDARD_WEEKMASK,
2768 xmex_rules(),
2769 Some(xmex_hours()),
2770 ),
2771 Xbue => Calendar::with_type(
2772 name,
2773 market_type("Equities"),
2774 STANDARD_WEEKMASK,
2775 xbue_rules(),
2776 Some(xbue_hours()),
2777 ),
2778 Xsgo => Calendar::with_type(
2779 name,
2780 market_type("Equities"),
2781 STANDARD_WEEKMASK,
2782 xsgo_rules(),
2783 Some(xsgo_hours()),
2784 ),
2785 Xlim => Calendar::with_type(
2786 name,
2787 market_type("Equities"),
2788 STANDARD_WEEKMASK,
2789 xlim_rules(),
2790 Some(xlim_hours()),
2791 ),
2792 Xbog => Calendar::with_type(
2793 name,
2794 market_type("Equities"),
2795 STANDARD_WEEKMASK,
2796 xbog_rules(),
2797 Some(xbog_hours()),
2798 ),
2799 }
2800}
2801
2802pub fn calendar_for_exchange(code: &str) -> Option<Calendar> {
2805 let upper = code.to_ascii_uppercase();
2806 if let Some(fam) = family_for_mic(&upper) {
2807 return Some(build_family(&upper, fam));
2808 }
2809
2810 let record = finance_enums::data::exchange_record(&upper)?;
2811 if let Some(mut calendar) = calendar_for_region(record.iso_country_code) {
2812 calendar.name = upper;
2813 return Some(calendar);
2814 }
2815
2816 Some(Calendar::with_type(
2817 upper,
2818 market_type_for_exchange_record(record),
2819 STANDARD_WEEKMASK,
2820 Vec::new(),
2821 None,
2822 ))
2823}
2824
2825fn market_type_for_exchange_record(record: &finance_enums::data::ExchangeRecord) -> &'static str {
2826 match record.market_category_code {
2827 "IDQS" | "NSPD" | "OTFS" | "SINT" => market_type("OverTheCounter"),
2828 _ => market_type("Equities"),
2829 }
2830}
2831
2832pub fn calendar_for_region(code: &str) -> Option<Calendar> {
2834 let upper = code.to_ascii_uppercase();
2835 if !COUNTRY_CODES.contains(&upper.as_str()) && !COUNTRY_CODES3.contains(&upper.as_str()) {
2836 return None;
2837 }
2838 match upper.as_str() {
2839 "US" | "USA" => calendar_for_exchange("XNYS"),
2840 "GB" | "GBR" => calendar_for_exchange("XLON"),
2841 "JP" | "JPN" => calendar_for_exchange("XTKS"),
2842 "HK" | "HKG" => calendar_for_exchange("XHKG"),
2843 "CN" | "CHN" => calendar_for_exchange("XSHG"),
2844 "DE" | "DEU" => calendar_for_exchange("XFRA"),
2845 "FR" | "FRA" => calendar_for_exchange("XPAR"),
2846 "CA" | "CAN" => calendar_for_exchange("XTSE"),
2847 "AU" | "AUS" => calendar_for_exchange("XASX"),
2848 "IN" | "IND" => calendar_for_exchange("XNSE"),
2849 "NL" | "NLD" => calendar_for_exchange("XAMS"),
2850 "BE" | "BEL" => calendar_for_exchange("XBRU"),
2851 "PT" | "PRT" => calendar_for_exchange("XLIS"),
2852 "IT" | "ITA" => calendar_for_exchange("XMIL"),
2853 "ES" | "ESP" => calendar_for_exchange("XMAD"),
2854 "CH" | "CHE" => calendar_for_exchange("XSWX"),
2855 "NO" | "NOR" => calendar_for_exchange("XOSL"),
2856 "SE" | "SWE" => calendar_for_exchange("XSTO"),
2857 "FI" | "FIN" => calendar_for_exchange("XHEL"),
2858 "DK" | "DNK" => calendar_for_exchange("XCSE"),
2859 "IS" | "ISL" => calendar_for_exchange("XICE"),
2860 "PL" | "POL" => calendar_for_exchange("XWAR"),
2861 "CZ" | "CZE" => calendar_for_exchange("XPRA"),
2862 "HU" | "HUN" => calendar_for_exchange("XBUD"),
2863 "AT" | "AUT" => calendar_for_exchange("XWBO"),
2864 "IE" | "IRL" => calendar_for_exchange("XDUB"),
2865 "KR" | "KOR" => calendar_for_exchange("XKRX"),
2866 "SG" | "SGP" => calendar_for_exchange("XSES"),
2867 "TW" | "TWN" => calendar_for_exchange("XTAI"),
2868 "TH" | "THA" => calendar_for_exchange("XBKK"),
2869 "MY" | "MYS" => calendar_for_exchange("XKLS"),
2870 "ID" | "IDN" => calendar_for_exchange("XIDX"),
2871 "PH" | "PHL" => calendar_for_exchange("XPHS"),
2872 "NZ" | "NZL" => calendar_for_exchange("XNZE"),
2873 "ZA" | "ZAF" => calendar_for_exchange("XJSE"),
2874 "SA" | "SAU" => calendar_for_exchange("XSAU"),
2875 "TR" | "TUR" => calendar_for_exchange("XIST"),
2876 "IL" | "ISR" => calendar_for_exchange("XTAE"),
2877 "AE" | "ARE" => calendar_for_exchange("XDFM"),
2878 "BR" | "BRA" => calendar_for_exchange("BVMF"),
2879 "MX" | "MEX" => calendar_for_exchange("XMEX"),
2880 "AR" | "ARG" => calendar_for_exchange("XBUE"),
2881 "CL" | "CHL" => calendar_for_exchange("XSGO"),
2882 "PE" | "PER" => calendar_for_exchange("XLIM"),
2883 "CO" | "COL" => calendar_for_exchange("XBOG"),
2884 _ => None,
2885 }
2886}
2887
2888pub fn calendar_for_product(exchange: &str, product: &str) -> Option<Calendar> {
2898 use Family::*;
2899 let exch = exchange.to_ascii_uppercase();
2900 let fam: Option<Family> = match (exch.as_str(), product) {
2901 ("XNYM", "Crude")
2904 | ("XNYM", "NaturalGas")
2905 | ("XNYM", "HeatingOil")
2906 | ("XNYM", "Gasoline")
2907 | ("XNYM", "LiquefiedNaturalGas")
2908 | ("XNYM", "Propane")
2909 | ("XNYM", "Electricity")
2910 | ("XNYM", "Uranium")
2911 | ("XNYM", "Energy") => Some(UsFuturesCmeEnergy),
2912 ("XNYM", "Gold")
2914 | ("XNYM", "Silver")
2915 | ("XNYM", "Copper")
2916 | ("XNYM", "Platinum")
2917 | ("XNYM", "Palladium")
2918 | ("XNYM", "Aluminum")
2919 | ("XNYM", "Zinc")
2920 | ("XNYM", "Nickel")
2921 | ("XNYM", "Lead")
2922 | ("XNYM", "Tin")
2923 | ("XNYM", "Steel")
2924 | ("XNYM", "Cobalt")
2925 | ("XNYM", "Iron")
2926 | ("XNYM", "Metals") => Some(UsFuturesCmeEnergy),
2927
2928 ("XCBT", "Corn")
2930 | ("XCBT", "Wheat")
2931 | ("XCBT", "Soybean")
2932 | ("XCBT", "Oats")
2933 | ("XCBT", "Soy")
2934 | ("XCBT", "Agriculture")
2935 | ("XCBT", "Softs") => Some(UsFuturesCbotGrains),
2936
2937 ("XCME", "Cattle") | ("XCME", "Feeder") | ("XCME", "Hogs") | ("XCME", "Livestock") => {
2939 Some(UsFuturesCmeLivestock)
2940 }
2941
2942 ("XCME", "Lumber") => Some(UsFuturesCmeLumber),
2944
2945 ("ICE_US", "Sugar")
2947 | ("ICE_US", "Coffee")
2948 | ("ICE_US", "Cocoa")
2949 | ("ICE_US", "Cotton")
2950 | ("ICE_US", "OrangeJuice")
2951 | ("ICE_US", "Crude")
2952 | ("ICE_US", "NaturalGas")
2953 | ("ICE_US", "Energy")
2954 | ("ICE_US", "Softs")
2955 | ("ICE_US", "Agriculture") => Some(UsFuturesIce),
2956
2957 _ => None,
2959 };
2960
2961 if let Some(family) = fam {
2962 let name = format!("{}:{}", exch, product);
2963 Some(build_family(&name, family))
2964 } else {
2965 calendar_for_exchange(exchange)
2966 }
2967}
2968
2969fn is_known_asset_label(value: &str) -> bool {
2970 UNDERLYING_ASSET_CLASSES.contains(&value)
2971 || COMMODITY_TYPES.contains(&value)
2972 || ENERGY_TYPES.contains(&value)
2973 || METALS_TYPES.contains(&value)
2974 || AGRICULTURE_TYPES.contains(&value)
2975 || matches!(value, "Feeder")
2976}
2977
2978pub fn calendar_for_asset(
2986 exchange: &str,
2987 asset_class: &str,
2988 subclass: Option<&str>,
2989) -> Option<Calendar> {
2990 if !is_known_asset_label(asset_class) {
2991 return None;
2992 }
2993 let product = subclass.unwrap_or(asset_class);
2994 if !is_known_asset_label(product) {
2995 return None;
2996 }
2997 calendar_for_product(exchange, product)
2998}
2999
3000#[cfg(test)]
3001mod tests {
3002 use super::*;
3003 use chrono::TimeZone;
3004 use chrono::Timelike;
3005
3006 #[test]
3007 fn nyse_2024_business_day_count() {
3008 let cal = calendar_for_exchange("XNYS").unwrap();
3009 let n = cal.business_days_between(
3010 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
3011 NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
3012 );
3013 assert_eq!(n, 252);
3014 }
3015
3016 #[test]
3017 fn nyse_christmas_2022_observed_monday() {
3018 let cal = calendar_for_exchange("XNYS").unwrap();
3019 assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2022, 12, 26).unwrap()));
3020 }
3021
3022 #[test]
3023 fn nyse_juneteenth_first_year_2021() {
3024 let cal = calendar_for_exchange("XNYS").unwrap();
3025 assert!(!cal.is_holiday(NaiveDate::from_ymd_opt(2020, 6, 19).unwrap()));
3026 assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2021, 6, 18).unwrap()));
3027 }
3028
3029 #[test]
3030 fn lse_easter_monday_2024() {
3031 let cal = calendar_for_exchange("XLON").unwrap();
3032 assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2024, 4, 1).unwrap()));
3033 }
3034
3035 #[test]
3036 fn region_us_resolves_to_xnys() {
3037 let cal = calendar_for_region("US").unwrap();
3038 assert_eq!(cal.name, "XNYS");
3039 assert_eq!(calendar_for_region("USA").unwrap().name, "XNYS");
3040 assert!(calendar_for_region("EU").is_none());
3041 assert!(calendar_for_region("UK").is_none());
3042 }
3043
3044 #[test]
3045 fn nyse_is_open_at_market_open() {
3046 let cal = calendar_for_exchange("XNYS").unwrap();
3047 let inst = chrono_tz::America::New_York
3048 .with_ymd_and_hms(2024, 1, 8, 9, 30, 0)
3049 .unwrap()
3050 .with_timezone(&Utc);
3051 assert!(cal.is_open(inst));
3052 let inst_b = chrono_tz::America::New_York
3053 .with_ymd_and_hms(2024, 1, 8, 9, 27, 0)
3054 .unwrap()
3055 .with_timezone(&Utc);
3056 assert!(!cal.is_open(inst_b));
3057 }
3058
3059 #[test]
3060 fn nyse_is_open_handles_dst() {
3061 let cal = calendar_for_exchange("XNYS").unwrap();
3062 let inst = chrono_tz::America::New_York
3063 .with_ymd_and_hms(2024, 3, 11, 9, 30, 0)
3064 .unwrap()
3065 .with_timezone(&Utc);
3066 assert!(cal.is_open(inst));
3067 }
3068
3069 #[test]
3070 fn cme_futures_open_sunday_evening() {
3071 let cal = calendar_for_exchange("XCME").unwrap();
3073 assert_eq!(cal.market_type, market_type("Futures"));
3074 let inst = chrono_tz::America::Chicago
3075 .with_ymd_and_hms(2024, 1, 7, 18, 0, 0)
3076 .unwrap()
3077 .with_timezone(&Utc);
3078 assert!(cal.is_open(inst));
3079 let inst2 = chrono_tz::America::Chicago
3081 .with_ymd_and_hms(2024, 1, 13, 3, 0, 0)
3082 .unwrap()
3083 .with_timezone(&Utc);
3084 assert!(!cal.is_open(inst2));
3085 }
3086
3087 #[test]
3088 fn nymex_energy_uses_chicago_tz() {
3089 let cal = calendar_for_exchange("XNYM").unwrap();
3090 assert_eq!(cal.market_type, market_type("Futures"));
3091 let inst = chrono_tz::America::Chicago
3093 .with_ymd_and_hms(2024, 1, 8, 9, 0, 0)
3094 .unwrap()
3095 .with_timezone(&Utc);
3096 assert!(cal.is_open(inst));
3097 }
3098
3099 #[test]
3100 fn nymex_energy_daily_maintenance_break_is_closed() {
3101 let cal = calendar_for_exchange("XNYM").unwrap();
3102 let maintenance_break = chrono_tz::America::Chicago
3103 .with_ymd_and_hms(2024, 1, 8, 16, 30, 0)
3104 .unwrap()
3105 .with_timezone(&Utc);
3106 let next_trade_date_open = chrono_tz::America::Chicago
3107 .with_ymd_and_hms(2024, 1, 8, 17, 0, 0)
3108 .unwrap()
3109 .with_timezone(&Utc);
3110
3111 assert!(!cal.is_open(maintenance_break));
3112 assert_eq!(cal.next_open(maintenance_break), Some(next_trade_date_open));
3113 }
3114
3115 #[test]
3116 fn cbot_grain_futures_expose_overnight_and_day_sessions() {
3117 let cal = calendar_for_exchange("CBOT_GRAINS").unwrap();
3118 assert_eq!(cal.market_type, market_type("Futures"));
3119 let th = cal.trading_hours.as_ref().unwrap();
3120 let actual: Vec<_> = th
3121 .sessions
3122 .iter()
3123 .map(|session| {
3124 (
3125 (
3126 session.open.hour(),
3127 session.open.minute(),
3128 session.open_day_offset,
3129 ),
3130 (
3131 session.close.hour(),
3132 session.close.minute(),
3133 session.close_day_offset,
3134 ),
3135 )
3136 })
3137 .collect();
3138
3139 assert_eq!(
3140 actual,
3141 vec![((19, 0, -1), (7, 45, 0)), ((8, 30, 0), (13, 20, 0))]
3142 );
3143
3144 let morning_break = chrono_tz::America::Chicago
3145 .with_ymd_and_hms(2024, 1, 8, 8, 0, 0)
3146 .unwrap()
3147 .with_timezone(&Utc);
3148 let day_open = chrono_tz::America::Chicago
3149 .with_ymd_and_hms(2024, 1, 8, 8, 30, 0)
3150 .unwrap()
3151 .with_timezone(&Utc);
3152 let day_close = chrono_tz::America::Chicago
3153 .with_ymd_and_hms(2024, 1, 8, 13, 20, 0)
3154 .unwrap()
3155 .with_timezone(&Utc);
3156
3157 assert!(!cal.is_open(morning_break));
3158 assert_eq!(cal.next_open(morning_break), Some(day_open));
3159 assert_eq!(cal.next_close(morning_break), Some(day_close));
3160 }
3161
3162 #[test]
3163 fn commodity_category_aliases_resolve_to_expected_templates() {
3164 for code in [
3165 "CBOT_OILSEEDS",
3166 "CBOT_WHEAT",
3167 "CBOT_CORN",
3168 "CBOT_SOYBEANS",
3169 "GLOBEX_GRAINS",
3170 "ZC",
3171 "ZW",
3172 "ZS",
3173 "ZL",
3174 "ZM",
3175 "ZO",
3176 "KE",
3177 "HRS",
3178 ] {
3179 let cal = calendar_for_exchange(code).unwrap();
3180 let th = cal.trading_hours.as_ref().unwrap();
3181 assert_eq!(th.sessions.len(), 2, "{code}");
3182 assert_eq!(th.sessions[0].open.hour(), 19, "{code}");
3183 assert_eq!(th.sessions[0].open_day_offset, -1, "{code}");
3184 assert_eq!(th.sessions[1].open.hour(), 8, "{code}");
3185 assert_eq!(th.sessions[1].open.minute(), 30, "{code}");
3186 }
3187
3188 for code in [
3189 "CME_ENERGY",
3190 "GLOBEX_ENERGY",
3191 "CME_METALS",
3192 "GLOBEX_METALS",
3193 "CL",
3194 "MCL",
3195 "QM",
3196 "GC",
3197 "MGC",
3198 "QO",
3199 ] {
3200 let cal = calendar_for_exchange(code).unwrap();
3201 let th = cal.trading_hours.as_ref().unwrap();
3202 assert_eq!(th.sessions.len(), 1, "{code}");
3203 assert_eq!(th.sessions[0].open.hour(), 17, "{code}");
3204 assert_eq!(th.sessions[0].open_day_offset, -1, "{code}");
3205 assert_eq!(th.sessions[0].close.hour(), 16, "{code}");
3206 }
3207
3208 for code in ["CME_DAIRY", "GLOBEX_DAIRY", "SR3", "ES", "NQ", "RTY"] {
3209 let cal = calendar_for_exchange(code).unwrap();
3210 let th = cal.trading_hours.as_ref().unwrap();
3211 assert_eq!(th.sessions.len(), 1, "{code}");
3212 assert_eq!(th.sessions[0].open.hour(), 17, "{code}");
3213 assert_eq!(th.sessions[0].open_day_offset, -1, "{code}");
3214 assert_eq!(th.sessions[0].close.hour(), 16, "{code}");
3215 }
3216
3217 for code in ["CME_LIVESTOCK", "GLOBEX_LIVESTOCK", "LE", "GF", "HE"] {
3218 let cal = calendar_for_exchange(code).unwrap();
3219 let th = cal.trading_hours.as_ref().unwrap();
3220 assert_eq!(th.sessions.len(), 1, "{code}");
3221 assert_eq!(th.sessions[0].open.hour(), 8, "{code}");
3222 assert_eq!(th.sessions[0].open.minute(), 30, "{code}");
3223 assert_eq!(th.sessions[0].open_day_offset, 0, "{code}");
3224 assert_eq!(th.sessions[0].close.hour(), 13, "{code}");
3225 assert_eq!(th.sessions[0].close.minute(), 5, "{code}");
3226 }
3227
3228 for code in ["CME_LUMBER", "GLOBEX_LUMBER", "LBR", "LS"] {
3229 let cal = calendar_for_exchange(code).unwrap();
3230 let th = cal.trading_hours.as_ref().unwrap();
3231 assert_eq!(th.sessions.len(), 1, "{code}");
3232 assert_eq!(th.sessions[0].open.hour(), 9, "{code}");
3233 assert_eq!(th.sessions[0].open_day_offset, 0, "{code}");
3234 assert_eq!(th.sessions[0].close.hour(), 15, "{code}");
3235 assert_eq!(th.sessions[0].close.minute(), 5, "{code}");
3236 }
3237 }
3238
3239 #[test]
3240 fn cfe_classifies_as_futures() {
3241 let cal = calendar_for_exchange("CFE").unwrap();
3242 assert_eq!(cal.market_type, market_type("Futures"));
3243 let inst = chrono_tz::America::Chicago
3245 .with_ymd_and_hms(2024, 1, 10, 9, 0, 0)
3246 .unwrap()
3247 .with_timezone(&Utc);
3248 assert!(cal.is_open(inst));
3249 }
3250
3251 #[test]
3252 fn forex_open_tuesday_3am() {
3253 let cal = calendar_for_exchange("FOREX").unwrap();
3254 assert_eq!(cal.market_type, market_type("ForeignExchange"));
3255 let inst = chrono_tz::America::New_York
3256 .with_ymd_and_hms(2024, 1, 9, 3, 0, 0)
3257 .unwrap()
3258 .with_timezone(&Utc);
3259 assert!(cal.is_open(inst));
3260 }
3261
3262 #[test]
3263 fn crypto_open_saturday_3am() {
3264 let cal = calendar_for_exchange("CRYPTO").unwrap();
3265 assert_eq!(cal.market_type, market_type("DigitalAssets"));
3266 let inst = chrono_tz::UTC
3267 .with_ymd_and_hms(2024, 1, 13, 3, 0, 0)
3268 .unwrap()
3269 .with_timezone(&Utc);
3270 assert!(cal.is_open(inst));
3271 }
3272
3273 #[test]
3274 fn options_close_at_1615() {
3275 let cal = calendar_for_exchange("OPRA").unwrap();
3276 assert_eq!(cal.market_type, market_type("Options"));
3277 let inst = chrono_tz::America::New_York
3278 .with_ymd_and_hms(2024, 1, 8, 16, 10, 0)
3279 .unwrap()
3280 .with_timezone(&Utc);
3281 assert!(cal.is_open(inst));
3282 }
3283
3284 #[test]
3285 fn sifma_includes_columbus_and_veterans() {
3286 let cal = calendar_for_exchange("SIFMA_US").unwrap();
3287 assert_eq!(cal.market_type, market_type("FixedIncome"));
3288 assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2024, 11, 11).unwrap()));
3290 assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2024, 10, 14).unwrap()));
3292 }
3293
3294 #[test]
3295 fn ice_us_uses_overnight_session() {
3296 let cal = calendar_for_exchange("ICE_US").unwrap();
3297 let inst = chrono_tz::America::New_York
3299 .with_ymd_and_hms(2024, 1, 7, 21, 0, 0)
3300 .unwrap()
3301 .with_timezone(&Utc);
3302 assert!(cal.is_open(inst));
3303 }
3304
3305 #[test]
3306 fn all_exchange_codes_resolve() {
3307 let mut missing = Vec::new();
3308 for code in EXCHANGE_CODES {
3309 if calendar_for_exchange(code).is_none() {
3310 missing.push(*code);
3311 }
3312 }
3313 assert!(missing.is_empty(), "unresolved MICs: {missing:?}");
3314 }
3315
3316 #[test]
3317 fn exchange_codes_are_sourced_from_finance_enums() {
3318 assert_eq!(EXCHANGE_CODES, finance_enums::data::ExchangeCode_VARIANTS);
3319 assert!(std::ptr::eq(
3320 EXCHANGE_CODES.as_ptr(),
3321 finance_enums::data::ExchangeCode_VARIANTS.as_ptr()
3322 ));
3323 }
3324
3325 #[test]
3326 fn market_type_variants_match_finance_enum_values() {
3327 let expected: &[&str] = &[
3328 "Equities",
3329 "FixedIncome",
3330 "ForeignExchange",
3331 "Commodities",
3332 "Derivatives",
3333 "Options",
3334 "Futures",
3335 "Funds",
3336 "DigitalAssets",
3337 "OverTheCounter",
3338 ];
3339 assert_eq!(MARKET_TYPES, expected);
3340 assert!(!MARKET_TYPES.contains(&"Other"));
3341 }
3342
3343 #[test]
3344 fn market_type_lookup_uses_finance_enum_variant_names() {
3345 assert_eq!(market_type("Options"), "Options");
3346 assert_eq!(market_type("Futures"), "Futures");
3347 assert!(MARKET_TYPES.contains(&market_type("Options")));
3348 }
3349
3350 #[test]
3351 fn calendar_for_asset_uses_finance_enum_asset_names() {
3352 let gas = calendar_for_asset("XNYM", "Commodity", Some("NaturalGas")).unwrap();
3353 assert_eq!(gas.market_type, market_type("Futures"));
3354 assert_eq!(gas.name, "XNYM:NaturalGas");
3355
3356 let grains = calendar_for_asset("XCBT", "Agriculture", None).unwrap();
3357 assert_eq!(grains.market_type, market_type("Futures"));
3358 assert_eq!(grains.name, "XCBT:Agriculture");
3359 assert_eq!(grains.trading_hours.unwrap().sessions.len(), 2);
3360
3361 let equity = calendar_for_asset("XNYS", "Equity", None).unwrap();
3362 assert_eq!(equity.market_type, market_type("Equities"));
3363 assert_eq!(equity.name, "XNYS");
3364
3365 assert!(calendar_for_asset("XNYS", "NotAnAssetClass", None).is_none());
3366 }
3367
3368 #[test]
3369 fn otc_inherits_nyse_holidays() {
3370 let cal = calendar_for_exchange("PINX").unwrap();
3371 assert_eq!(cal.market_type, market_type("Equities"));
3372 assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2024, 7, 4).unwrap()));
3373 }
3374
3375 #[test]
3376 fn canadian_calendar_for_neoe() {
3377 let cal = calendar_for_exchange("NEOE").unwrap();
3378 assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2024, 7, 1).unwrap()));
3380 }
3381
3382 #[test]
3383 fn nyse_july3_2024_is_early_close() {
3384 let cal = calendar_for_exchange("XNYS").unwrap();
3386 let day = NaiveDate::from_ymd_opt(2024, 7, 3).unwrap();
3387 assert_eq!(
3388 cal.early_close_for(day),
3389 Some(NaiveTime::from_hms_opt(13, 0, 0).unwrap())
3390 );
3391 let inst = chrono_tz::America::New_York
3393 .with_ymd_and_hms(2024, 7, 3, 14, 0, 0)
3394 .unwrap()
3395 .with_timezone(&Utc);
3396 assert!(!cal.is_open(inst));
3397 let inst2 = chrono_tz::America::New_York
3399 .with_ymd_and_hms(2024, 7, 3, 12, 30, 0)
3400 .unwrap()
3401 .with_timezone(&Utc);
3402 assert!(cal.is_open(inst2));
3403 }
3404
3405 #[test]
3406 fn nyse_black_friday_2024_early_close() {
3407 let cal = calendar_for_exchange("XNYS").unwrap();
3409 assert_eq!(
3410 cal.early_close_for(NaiveDate::from_ymd_opt(2024, 11, 29).unwrap()),
3411 Some(NaiveTime::from_hms_opt(13, 0, 0).unwrap())
3412 );
3413 }
3414
3415 #[test]
3416 fn xams_kingsday_2024() {
3417 let cal = calendar_for_exchange("XAMS").unwrap();
3420 assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2023, 4, 27).unwrap()));
3421 }
3422
3423 #[test]
3424 fn xkrx_seollal_2024_multi_day() {
3425 let cal = calendar_for_exchange("XKRX").unwrap();
3427 assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2024, 2, 9).unwrap()));
3428 assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2024, 2, 12).unwrap()));
3429 }
3430
3431 #[test]
3432 fn xtae_uses_sun_thu_weekmask() {
3433 let cal = calendar_for_exchange("XTAE").unwrap();
3434 assert!(cal.is_business_day(NaiveDate::from_ymd_opt(2024, 5, 5).unwrap()));
3436 assert!(!cal.is_business_day(NaiveDate::from_ymd_opt(2024, 5, 3).unwrap()));
3438 }
3439
3440 #[test]
3441 fn xsau_uses_sun_thu_weekmask() {
3442 let cal = calendar_for_exchange("XSAU").unwrap();
3443 assert!(cal.is_business_day(NaiveDate::from_ymd_opt(2024, 5, 5).unwrap()));
3444 assert!(!cal.is_business_day(NaiveDate::from_ymd_opt(2024, 5, 3).unwrap()));
3445 }
3446
3447 #[test]
3448 fn bvmf_carnival_2024() {
3449 let cal = calendar_for_exchange("BVMF").unwrap();
3451 assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2024, 2, 12).unwrap()));
3452 assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2024, 2, 13).unwrap()));
3453 }
3454
3455 #[test]
3456 fn region_br_resolves_to_bvmf() {
3457 let cal = calendar_for_region("BR").unwrap();
3458 assert_eq!(cal.name, "BVMF");
3459 assert_eq!(calendar_for_region("BRA").unwrap().name, "BVMF");
3460 }
3461
3462 #[test]
3463 fn xnze_waitangi_2024() {
3464 let cal = calendar_for_exchange("XNZE").unwrap();
3465 assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2024, 2, 6).unwrap()));
3467 }
3468
3469 #[test]
3470 fn apac_lunch_break_calendars_expose_split_sessions() {
3471 let cases = [
3472 ("XTKS", vec![((9, 0), (11, 30)), ((12, 30), (15, 30))]),
3473 ("XHKG", vec![((9, 30), (12, 0)), ((13, 0), (16, 0))]),
3474 ("XSHG", vec![((9, 30), (11, 30)), ((13, 0), (15, 0))]),
3475 ];
3476
3477 for (code, expected) in cases {
3478 let cal = calendar_for_exchange(code).unwrap();
3479 let th = cal.trading_hours.as_ref().unwrap();
3480 let actual: Vec<_> = th
3481 .sessions
3482 .iter()
3483 .map(|session| {
3484 (
3485 (session.open.hour(), session.open.minute()),
3486 (session.close.hour(), session.close.minute()),
3487 )
3488 })
3489 .collect();
3490 assert_eq!(actual, expected, "{code} sessions");
3491 }
3492 }
3493
3494 #[test]
3495 fn tokyo_lunch_gap_is_closed_and_boundaries_advance() {
3496 let cal = calendar_for_exchange("XTKS").unwrap();
3497 let lunch_gap = chrono_tz::Asia::Tokyo
3498 .with_ymd_and_hms(2026, 5, 25, 11, 45, 0)
3499 .unwrap()
3500 .with_timezone(&Utc);
3501 let afternoon_open = chrono_tz::Asia::Tokyo
3502 .with_ymd_and_hms(2026, 5, 25, 12, 30, 0)
3503 .unwrap()
3504 .with_timezone(&Utc);
3505 let afternoon_close = chrono_tz::Asia::Tokyo
3506 .with_ymd_and_hms(2026, 5, 25, 15, 30, 0)
3507 .unwrap()
3508 .with_timezone(&Utc);
3509
3510 assert!(!cal.is_open(lunch_gap));
3511 assert_eq!(cal.next_open(lunch_gap), Some(afternoon_open));
3512 assert_eq!(cal.next_close(lunch_gap), Some(afternoon_close));
3513
3514 let sessions = cal.sessions_between(
3515 NaiveDate::from_ymd_opt(2026, 5, 25).unwrap(),
3516 NaiveDate::from_ymd_opt(2026, 5, 25).unwrap(),
3517 );
3518 assert_eq!(sessions.len(), 2);
3519 }
3520
3521 #[test]
3522 fn tokyo_uses_historical_close_before_2024_schedule_change() {
3523 let cal = calendar_for_exchange("XTKS").unwrap();
3524 let before = cal.sessions_between(
3525 NaiveDate::from_ymd_opt(2024, 11, 1).unwrap(),
3526 NaiveDate::from_ymd_opt(2024, 11, 1).unwrap(),
3527 );
3528 let after = cal.sessions_between(
3529 NaiveDate::from_ymd_opt(2024, 11, 5).unwrap(),
3530 NaiveDate::from_ymd_opt(2024, 11, 5).unwrap(),
3531 );
3532
3533 let before_close = before[1].1.with_timezone(&chrono_tz::Asia::Tokyo);
3534 let after_close = after[1].1.with_timezone(&chrono_tz::Asia::Tokyo);
3535 assert_eq!((before_close.hour(), before_close.minute()), (15, 0));
3536 assert_eq!((after_close.hour(), after_close.minute()), (15, 30));
3537
3538 let old_late_afternoon = chrono_tz::Asia::Tokyo
3539 .with_ymd_and_hms(2024, 11, 1, 15, 15, 0)
3540 .unwrap()
3541 .with_timezone(&Utc);
3542 let current_late_afternoon = chrono_tz::Asia::Tokyo
3543 .with_ymd_and_hms(2024, 11, 5, 15, 15, 0)
3544 .unwrap()
3545 .with_timezone(&Utc);
3546 assert!(!cal.is_open(old_late_afternoon));
3547 assert!(cal.is_open(current_late_afternoon));
3548 }
3549
3550 #[test]
3551 fn session_boundaries_are_explicitly_inclusive_for_next_boundaries() {
3552 let cal = calendar_for_exchange("XTKS").unwrap();
3553 let exact_afternoon_open = chrono_tz::Asia::Tokyo
3554 .with_ymd_and_hms(2026, 5, 25, 12, 30, 0)
3555 .unwrap()
3556 .with_timezone(&Utc);
3557 let exact_morning_close = chrono_tz::Asia::Tokyo
3558 .with_ymd_and_hms(2026, 5, 25, 11, 30, 0)
3559 .unwrap()
3560 .with_timezone(&Utc);
3561
3562 assert!(cal.is_open(exact_afternoon_open));
3563 assert_eq!(
3564 cal.next_open(exact_afternoon_open),
3565 Some(exact_afternoon_open)
3566 );
3567 assert!(!cal.is_open(exact_morning_close));
3568 assert_eq!(
3569 cal.next_close(exact_morning_close),
3570 Some(exact_morning_close)
3571 );
3572 }
3573
3574 #[test]
3575 fn nyse_sessions_between_one_week_with_early_close() {
3576 let cal = calendar_for_exchange("XNYS").unwrap();
3577 let s = cal.sessions_between(
3579 NaiveDate::from_ymd_opt(2024, 7, 1).unwrap(),
3580 NaiveDate::from_ymd_opt(2024, 7, 5).unwrap(),
3581 );
3582 assert_eq!(s.len(), 4);
3583 let jul3_close_local = s[2].1.with_timezone(&chrono_tz::America::New_York);
3585 assert_eq!(jul3_close_local.hour(), 13);
3586 assert_eq!(jul3_close_local.minute(), 0);
3587 let jul5_close_local = s[3].1.with_timezone(&chrono_tz::America::New_York);
3589 assert_eq!(jul5_close_local.hour(), 16);
3590 }
3591
3592 #[test]
3593 fn nyse_extended_sessions_include_pre_open_and_after_close() {
3594 let cal = calendar_for_exchange("XNYS").unwrap();
3595 let s = cal.extended_sessions_between(
3596 NaiveDate::from_ymd_opt(2024, 1, 8).unwrap(),
3597 NaiveDate::from_ymd_opt(2024, 1, 8).unwrap(),
3598 );
3599 assert_eq!(s.len(), 2);
3600 assert_eq!(s[0].0, "pre_open");
3601 assert_eq!(s[1].0, "after_close");
3602
3603 let pre_open_local = s[0].1.with_timezone(&chrono_tz::America::New_York);
3604 let pre_close_local = s[0].2.with_timezone(&chrono_tz::America::New_York);
3605 assert_eq!((pre_open_local.hour(), pre_open_local.minute()), (4, 0));
3606 assert_eq!((pre_close_local.hour(), pre_close_local.minute()), (9, 30));
3607
3608 let after_open_local = s[1].1.with_timezone(&chrono_tz::America::New_York);
3609 let after_close_local = s[1].2.with_timezone(&chrono_tz::America::New_York);
3610 assert_eq!(
3611 (after_open_local.hour(), after_open_local.minute()),
3612 (16, 0)
3613 );
3614 assert_eq!(
3615 (after_close_local.hour(), after_close_local.minute()),
3616 (20, 0)
3617 );
3618 }
3619
3620 #[test]
3621 fn nyse_after_close_starts_at_early_close() {
3622 let cal = calendar_for_exchange("XNYS").unwrap();
3623 let s = cal.extended_sessions_between(
3624 NaiveDate::from_ymd_opt(2024, 7, 3).unwrap(),
3625 NaiveDate::from_ymd_opt(2024, 7, 3).unwrap(),
3626 );
3627 let after_open_local = s[1].1.with_timezone(&chrono_tz::America::New_York);
3628 let after_close_local = s[1].2.with_timezone(&chrono_tz::America::New_York);
3629 assert_eq!(
3630 (after_open_local.hour(), after_open_local.minute()),
3631 (13, 0)
3632 );
3633 assert_eq!(
3634 (after_close_local.hour(), after_close_local.minute()),
3635 (20, 0)
3636 );
3637 }
3638
3639 #[test]
3640 fn nyse_holidays_between_q3_2024() {
3641 let cal = calendar_for_exchange("XNYS").unwrap();
3642 let h = cal.holidays_between(
3643 NaiveDate::from_ymd_opt(2024, 7, 1).unwrap(),
3644 NaiveDate::from_ymd_opt(2024, 9, 30).unwrap(),
3645 );
3646 assert!(h.contains(&NaiveDate::from_ymd_opt(2024, 7, 4).unwrap()));
3648 assert!(h.contains(&NaiveDate::from_ymd_opt(2024, 9, 2).unwrap()));
3649 assert_eq!(h.len(), 2);
3650 }
3651}