Skip to main content

finance_dates/
calendar.rs

1//! Per-exchange / per-region calendars.
2//!
3//! Distinguishes equity, options, futures, FX, bond, and crypto markets, each
4//! of which can have very different holiday calendars and trading hours. The
5//! `calendar_for_exchange` lookup covers every MIC currently exposed by
6//! finance-enums plus a few common futures venues.
7
8use 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::{
14    business_day_range, business_days_between, next_business_day, previous_business_day,
15    STANDARD_WEEKMASK,
16};
17use crate::trading_hours::{Session, TradingHours};
18
19pub use finance_enums::data::ExchangeCode_VARIANTS as EXCHANGE_CODES;
20
21/// The class of instrument a calendar represents.
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum MarketType {
24    /// Cash equities and ETFs.
25    Equity,
26    /// Listed options.
27    Options,
28    /// Listed futures and futures options.
29    Futures,
30    /// Spot / margin FX.
31    Fx,
32    /// Fixed-income (SIFMA-style).
33    Bond,
34    /// 24x7 crypto.
35    Crypto,
36    /// Other / unknown.
37    Other,
38}
39
40impl MarketType {
41    pub fn as_str(self) -> &'static str {
42        match self {
43            MarketType::Equity => "equity",
44            MarketType::Options => "options",
45            MarketType::Futures => "futures",
46            MarketType::Fx => "fx",
47            MarketType::Bond => "bond",
48            MarketType::Crypto => "crypto",
49            MarketType::Other => "other",
50        }
51    }
52}
53
54/// Mon-Sun all-true weekmask (used by 24x7 crypto venues).
55pub const CRYPTO_WEEKMASK: [bool; 7] = [true, true, true, true, true, true, true];
56
57/// Sun-Fri weekmask used by 24x5 FX. Monday=index 0; Sunday=index 6.
58pub const FX_WEEKMASK: [bool; 7] = [true, true, true, true, true, false, true];
59
60/// ISO region codes recognised by `calendar_for_region`.
61pub const REGION_CODES: &[&str] = &[
62    "US", "UK", "GB", "EU", "JP", "HK", "CN", "CA", "AU", "IN", "DE", "FR", "NL", "BE", "PT", "IT",
63    "ES", "CH", "NO", "SE", "FI", "DK", "IS", "PL", "CZ", "HU", "AT", "IE", "KR", "SG", "TW", "TH",
64    "MY", "ID", "PH", "NZ", "ZA", "SA", "TR", "IL", "AE", "BR", "MX", "AR", "CL", "PE", "CO",
65];
66
67/// A holiday calendar with optional trading hours and a market classification.
68pub struct Calendar {
69    pub name: String,
70    pub market_type: MarketType,
71    pub weekmask: [bool; 7],
72    pub rules: Vec<HolidayRule>,
73    pub trading_hours: Option<TradingHours>,
74    /// Days when the venue closes earlier than usual. Each rule resolves to
75    /// at most one date per year, paired with a local close time that
76    /// replaces the normal session close on that date.
77    pub early_closes: Vec<EarlyCloseRule>,
78    cache: HolidayCache,
79    early_cache: EarlyCloseCache,
80}
81
82/// An early-close rule. `rule` resolves to a date (using the same machinery
83/// as holiday rules); `close_time` is the local time at which the venue
84/// closes on that date instead of its regular session close.
85#[derive(Clone, Debug)]
86pub struct EarlyCloseRule {
87    pub rule: HolidayRule,
88    pub close_time: NaiveTime,
89}
90
91#[derive(Default)]
92struct EarlyCloseCache {
93    inner: parking_lot_dummy::RwLock<
94        std::collections::HashMap<i32, Arc<std::collections::HashMap<NaiveDate, NaiveTime>>>,
95    >,
96}
97
98#[derive(Default)]
99struct HolidayCache {
100    inner: parking_lot_dummy::RwLock<std::collections::HashMap<i32, Arc<BTreeSet<NaiveDate>>>>,
101}
102
103mod parking_lot_dummy {
104    use std::sync::RwLock as StdRwLock;
105    pub struct RwLock<T>(pub StdRwLock<T>);
106    impl<T: Default> Default for RwLock<T> {
107        fn default() -> Self {
108            Self(StdRwLock::new(T::default()))
109        }
110    }
111    impl<T> RwLock<T> {
112        pub fn read(&self) -> std::sync::RwLockReadGuard<'_, T> {
113            self.0.read().unwrap()
114        }
115        pub fn write(&self) -> std::sync::RwLockWriteGuard<'_, T> {
116            self.0.write().unwrap()
117        }
118    }
119}
120
121impl Calendar {
122    pub fn new(
123        name: impl Into<String>,
124        weekmask: [bool; 7],
125        rules: Vec<HolidayRule>,
126        trading_hours: Option<TradingHours>,
127    ) -> Self {
128        Self::with_type(name, MarketType::Equity, weekmask, rules, trading_hours)
129    }
130
131    pub fn with_type(
132        name: impl Into<String>,
133        market_type: MarketType,
134        weekmask: [bool; 7],
135        rules: Vec<HolidayRule>,
136        trading_hours: Option<TradingHours>,
137    ) -> Self {
138        Self {
139            name: name.into(),
140            market_type,
141            weekmask,
142            rules,
143            trading_hours,
144            early_closes: Vec::new(),
145            cache: HolidayCache::default(),
146            early_cache: EarlyCloseCache::default(),
147        }
148    }
149
150    /// Builder: attach early-close rules.
151    pub fn with_early_closes(mut self, ec: Vec<EarlyCloseRule>) -> Self {
152        self.early_closes = ec;
153        self
154    }
155
156    /// Cached map of `date -> local early-close time` for the given year.
157    fn early_close_map(&self, year: i32) -> Arc<std::collections::HashMap<NaiveDate, NaiveTime>> {
158        if let Some(m) = self.early_cache.inner.read().get(&year).cloned() {
159            return m;
160        }
161        let mut m = std::collections::HashMap::new();
162        for ec in &self.early_closes {
163            if let Some(d) = ec.rule.observed_in(year) {
164                // Only register if the resulting date is itself a business
165                // day (skip rolled-into-weekend cases).
166                let i = d.weekday().num_days_from_monday() as usize;
167                if !self.weekmask[i] {
168                    continue;
169                }
170                if self.holidays(year).contains(&d) {
171                    continue;
172                }
173                m.insert(d, ec.close_time);
174            }
175        }
176        let arc = Arc::new(m);
177        self.early_cache.inner.write().insert(year, arc.clone());
178        arc
179    }
180
181    /// Local early-close time for `date`, if any.
182    pub fn early_close_for(&self, date: NaiveDate) -> Option<NaiveTime> {
183        self.early_close_map(date.year()).get(&date).copied()
184    }
185
186    pub fn holidays(&self, year: i32) -> Arc<BTreeSet<NaiveDate>> {
187        if let Some(h) = self.cache.inner.read().get(&year).cloned() {
188            return h;
189        }
190        let mut set = BTreeSet::new();
191        for r in &self.rules {
192            for d in r.dates_in(year) {
193                set.insert(d);
194            }
195        }
196        let arc = Arc::new(set);
197        self.cache.inner.write().insert(year, arc.clone());
198        arc
199    }
200
201    pub fn holidays_between(&self, start: NaiveDate, end: NaiveDate) -> BTreeSet<NaiveDate> {
202        let mut out = BTreeSet::new();
203        for y in start.year()..=end.year() {
204            for d in self.holidays(y).iter() {
205                if *d >= start && *d <= end {
206                    out.insert(*d);
207                }
208            }
209        }
210        out
211    }
212
213    pub fn is_holiday(&self, d: NaiveDate) -> bool {
214        self.holidays(d.year()).contains(&d)
215    }
216
217    pub fn is_business_day(&self, d: NaiveDate) -> bool {
218        let i = d.weekday().num_days_from_monday() as usize;
219        self.weekmask[i] && !self.is_holiday(d)
220    }
221
222    pub fn next_business_day(&self, d: NaiveDate) -> NaiveDate {
223        let years = [d.year(), d.year() + 1];
224        let mut h = BTreeSet::new();
225        for y in years {
226            for x in self.holidays(y).iter() {
227                h.insert(*x);
228            }
229        }
230        next_business_day(d, &self.weekmask, &h)
231    }
232
233    pub fn previous_business_day(&self, d: NaiveDate) -> NaiveDate {
234        let years = [d.year() - 1, d.year()];
235        let mut h = BTreeSet::new();
236        for y in years {
237            for x in self.holidays(y).iter() {
238                h.insert(*x);
239            }
240        }
241        previous_business_day(d, &self.weekmask, &h)
242    }
243
244    pub fn business_days_between(&self, start: NaiveDate, end: NaiveDate) -> i64 {
245        let h = self.holidays_between(start, end);
246        business_days_between(start, end, &self.weekmask, &h)
247    }
248
249    pub fn business_day_range(&self, start: NaiveDate, end: NaiveDate) -> Vec<NaiveDate> {
250        let h = self.holidays_between(start, end);
251        business_day_range(start, end, &self.weekmask, &h)
252    }
253
254    /// True iff the venue is currently in any trading session.
255    ///
256    /// For sessions that span midnight, the trading-day check considers both
257    /// the local calendar day of `when` and the next local calendar day, so a
258    /// Sun-evening CME open correctly maps to Mon's trading day. If an
259    /// early-close is in effect for that trading day, the last session's
260    /// close is shortened.
261    pub fn is_open(&self, when: DateTime<Utc>) -> bool {
262        let Some(th) = &self.trading_hours else {
263            return false;
264        };
265        let local_today = when.with_timezone(&th.timezone).date_naive();
266        for delta in [0i64, 1] {
267            let trading_day = local_today + Duration::days(delta);
268            if !self.is_business_day(trading_day) {
269                continue;
270            }
271            let early = self.early_close_for(trading_day);
272            let last_idx = th.sessions.len().saturating_sub(1);
273            for (i, s) in th.sessions.iter().enumerate() {
274                let Some((o, mut c)) = s.instants(th.timezone, trading_day) else {
275                    continue;
276                };
277                if i == last_idx {
278                    if let Some(t) = early {
279                        if let Some(early_c) = adjust_close(th.timezone, trading_day, s, t) {
280                            c = early_c;
281                        }
282                    }
283                }
284                if when >= o && when < c {
285                    return true;
286                }
287            }
288        }
289        false
290    }
291
292    pub fn next_open(&self, when: DateTime<Utc>) -> Option<DateTime<Utc>> {
293        let th = self.trading_hours.as_ref()?;
294        let local_today = when.with_timezone(&th.timezone).date_naive();
295        for delta in 0..400i64 {
296            let trading_day = local_today + Duration::days(delta);
297            if !self.is_business_day(trading_day) {
298                continue;
299            }
300            for s in &th.sessions {
301                if let Some((o, _)) = s.instants(th.timezone, trading_day) {
302                    if o >= when {
303                        return Some(o);
304                    }
305                }
306            }
307        }
308        None
309    }
310
311    pub fn next_close(&self, when: DateTime<Utc>) -> Option<DateTime<Utc>> {
312        let th = self.trading_hours.as_ref()?;
313        let local_today = when.with_timezone(&th.timezone).date_naive();
314        for delta in 0..400i64 {
315            let trading_day = local_today + Duration::days(delta);
316            if !self.is_business_day(trading_day) {
317                continue;
318            }
319            let early = self.early_close_for(trading_day);
320            let last_idx = th.sessions.len().saturating_sub(1);
321            for (i, s) in th.sessions.iter().enumerate() {
322                let Some((_, mut c)) = s.instants(th.timezone, trading_day) else {
323                    continue;
324                };
325                if i == last_idx {
326                    if let Some(t) = early {
327                        if let Some(early_c) = adjust_close(th.timezone, trading_day, s, t) {
328                            c = early_c;
329                        }
330                    }
331                }
332                if c >= when {
333                    return Some(c);
334                }
335            }
336        }
337        None
338    }
339
340    /// All `(open, close)` UTC instants for every business day in
341    /// `[start, end]` (inclusive). Each business day contributes one entry
342    /// per trading session, with the last session's close adjusted for any
343    /// early-close rule. Returns an empty vector when no trading hours are
344    /// configured.
345    pub fn sessions_between(
346        &self,
347        start: NaiveDate,
348        end: NaiveDate,
349    ) -> Vec<(DateTime<Utc>, DateTime<Utc>)> {
350        let Some(th) = &self.trading_hours else {
351            return Vec::new();
352        };
353        let mut out = Vec::new();
354        let last_idx = th.sessions.len().saturating_sub(1);
355        let mut d = start;
356        while d <= end {
357            if self.is_business_day(d) {
358                let early = self.early_close_for(d);
359                for (i, s) in th.sessions.iter().enumerate() {
360                    let Some((o, mut c)) = s.instants(th.timezone, d) 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, d, s, t) {
366                                c = early_c;
367                            }
368                        }
369                    }
370                    out.push((o, c));
371                }
372            }
373            d += Duration::days(1);
374        }
375        out
376    }
377}
378
379/// Recompute the close instant of `session` on `trading_day` using the
380/// override `local_close_time`. The day-offset of the original close is
381/// preserved so cross-midnight sessions still resolve correctly.
382fn adjust_close(
383    tz: chrono_tz::Tz,
384    trading_day: NaiveDate,
385    session: &Session,
386    local_close_time: NaiveTime,
387) -> Option<DateTime<Utc>> {
388    use chrono::TimeZone;
389    let close_local_day = trading_day + Duration::days(session.close_day_offset as i64);
390    let close = tz
391        .from_local_datetime(&close_local_day.and_time(local_close_time))
392        .single()?;
393    Some(close.with_timezone(&Utc))
394}
395
396// ---------- Holiday rule constructors ----------
397
398fn fixed(month: u32, day: u32, since_year: Option<i32>) -> HolidayRule {
399    HolidayRule::Fixed {
400        month,
401        day,
402        roll: WeekendRoll::NearestWeekday,
403        since_year,
404    }
405}
406
407fn fixed_no_roll(month: u32, day: u32, since_year: Option<i32>) -> HolidayRule {
408    HolidayRule::Fixed {
409        month,
410        day,
411        roll: WeekendRoll::None,
412        since_year,
413    }
414}
415
416fn nth(month: u32, weekday: Weekday, n: i32) -> HolidayRule {
417    HolidayRule::NthWeekday {
418        month,
419        weekday,
420        n,
421        since_year: None,
422    }
423}
424
425fn easter(offset_days: i32) -> HolidayRule {
426    HolidayRule::EasterOffset {
427        offset_days,
428        since_year: None,
429    }
430}
431
432// ---------- Built-in calendars ----------
433
434fn nyse_rules() -> Vec<HolidayRule> {
435    vec![
436        fixed(1, 1, None),
437        nth(1, Weekday::Mon, 3),
438        nth(2, Weekday::Mon, 3),
439        easter(-2),
440        nth(5, Weekday::Mon, -1),
441        fixed(6, 19, Some(2021)),
442        fixed(7, 4, None),
443        nth(9, Weekday::Mon, 1),
444        nth(11, Weekday::Thu, 4),
445        fixed(12, 25, None),
446    ]
447}
448
449fn nyse_trading_hours() -> TradingHours {
450    TradingHours::new(
451        NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
452        NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
453        chrono_tz::America::New_York,
454    )
455}
456
457/// Listed options use the same holidays as NYSE; market hours run from 09:30
458/// to 16:15 ET (index options often 16:15, single-name 16:00). We pick 16:15
459/// as the default close to maximize coverage.
460fn options_trading_hours() -> TradingHours {
461    TradingHours::new(
462        NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
463        NaiveTime::from_hms_opt(16, 15, 0).unwrap(),
464        chrono_tz::America::New_York,
465    )
466}
467
468/// CME Globex baseline holidays — only days when *all* products close:
469/// New Year's Day, Good Friday, Christmas. Product-specific closures
470/// (Memorial Day, Independence Day, Thanksgiving, …) are typically partial
471/// closes / early closes which this layer does not yet model.
472fn cme_globex_rules() -> Vec<HolidayRule> {
473    vec![fixed(1, 1, None), easter(-2), fixed(12, 25, None)]
474}
475
476/// CME Globex equity-index, FX, fixed-income futures: 17:00 prev — 16:00 today CT.
477fn cme_globex_overnight_hours() -> TradingHours {
478    TradingHours::from_sessions(
479        vec![Session::overnight(
480            NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
481            NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
482        )],
483        chrono_tz::America::Chicago,
484    )
485}
486
487/// CME Globex energy & metals: 17:00 prev — 16:00 today CT (with one-hour
488/// daily break that this layer treats as a single contiguous session).
489fn cme_globex_energy_hours() -> TradingHours {
490    TradingHours::from_sessions(
491        vec![Session::overnight(
492            NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
493            NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
494        )],
495        chrono_tz::America::Chicago,
496    )
497}
498
499/// CBOE Futures Exchange (CFE) — full US holiday set, 08:30–15:15 CT regular.
500fn cfe_rules() -> Vec<HolidayRule> {
501    vec![
502        fixed(1, 1, None),
503        nth(1, Weekday::Mon, 3),
504        nth(2, Weekday::Mon, 3),
505        easter(-2),
506        nth(5, Weekday::Mon, -1),
507        fixed(6, 19, Some(2022)),
508        fixed(7, 4, None),
509        nth(9, Weekday::Mon, 1),
510        nth(11, Weekday::Thu, 4),
511        fixed(12, 25, None),
512    ]
513}
514
515fn cfe_trading_hours() -> TradingHours {
516    TradingHours::new(
517        NaiveTime::from_hms_opt(8, 30, 0).unwrap(),
518        NaiveTime::from_hms_opt(15, 15, 0).unwrap(),
519        chrono_tz::America::Chicago,
520    )
521}
522
523/// ICE US futures (energy, softs): 20:00 prev — 18:00 today ET.
524fn ice_us_rules() -> Vec<HolidayRule> {
525    vec![fixed(1, 1, None), easter(-2), fixed(12, 25, None)]
526}
527
528fn ice_us_hours() -> TradingHours {
529    TradingHours::from_sessions(
530        vec![Session::overnight(
531            NaiveTime::from_hms_opt(20, 0, 0).unwrap(),
532            NaiveTime::from_hms_opt(18, 0, 0).unwrap(),
533        )],
534        chrono_tz::America::New_York,
535    )
536}
537
538/// SIFMA US bond market — recommended fixed-income closures.
539fn sifma_us_rules() -> Vec<HolidayRule> {
540    vec![
541        fixed(1, 1, None),
542        nth(1, Weekday::Mon, 3),
543        nth(2, Weekday::Mon, 3),
544        easter(-2),
545        nth(5, Weekday::Mon, -1),
546        fixed(6, 19, Some(2022)),
547        fixed(7, 4, None),
548        nth(9, Weekday::Mon, 1),
549        nth(10, Weekday::Mon, 2), // Columbus Day
550        fixed(11, 11, None),      // Veterans Day
551        nth(11, Weekday::Thu, 4),
552        fixed(12, 25, None),
553    ]
554}
555
556fn sifma_us_hours() -> TradingHours {
557    TradingHours::new(
558        NaiveTime::from_hms_opt(7, 0, 0).unwrap(),
559        NaiveTime::from_hms_opt(17, 30, 0).unwrap(),
560        chrono_tz::America::New_York,
561    )
562}
563
564/// 24x5 spot FX — opens Sun 17:00 NY, closes Fri 17:00 NY. New Year's Day +
565/// Christmas remain closed; otherwise no holidays.
566fn forex_rules() -> Vec<HolidayRule> {
567    vec![fixed(1, 1, None), fixed(12, 25, None)]
568}
569
570/// 24x7 crypto — no holidays.
571fn crypto_rules() -> Vec<HolidayRule> {
572    vec![]
573}
574
575fn lse_rules() -> Vec<HolidayRule> {
576    vec![
577        fixed(1, 1, None),
578        easter(-2),
579        easter(1),
580        nth(5, Weekday::Mon, 1),
581        nth(5, Weekday::Mon, -1),
582        nth(8, Weekday::Mon, -1),
583        fixed(12, 25, None),
584        fixed(12, 26, None),
585    ]
586}
587
588fn lse_trading_hours() -> TradingHours {
589    TradingHours::new(
590        NaiveTime::from_hms_opt(8, 0, 0).unwrap(),
591        NaiveTime::from_hms_opt(16, 30, 0).unwrap(),
592        chrono_tz::Europe::London,
593    )
594}
595
596fn tse_rules() -> Vec<HolidayRule> {
597    vec![
598        fixed_no_roll(1, 1, None),
599        fixed_no_roll(1, 2, None),
600        fixed_no_roll(1, 3, None),
601        nth(1, Weekday::Mon, 2),
602        fixed_no_roll(2, 11, None),
603        fixed_no_roll(2, 23, Some(2020)),
604        fixed_no_roll(4, 29, None),
605        fixed_no_roll(5, 3, None),
606        fixed_no_roll(5, 4, None),
607        fixed_no_roll(5, 5, None),
608        nth(7, Weekday::Mon, 3),
609        fixed_no_roll(8, 11, None),
610        nth(9, Weekday::Mon, 3),
611        nth(10, Weekday::Mon, 2),
612        fixed_no_roll(11, 3, None),
613        fixed_no_roll(11, 23, None),
614        fixed_no_roll(12, 31, None),
615    ]
616}
617
618fn tse_trading_hours() -> TradingHours {
619    TradingHours::new(
620        NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
621        NaiveTime::from_hms_opt(15, 0, 0).unwrap(),
622        chrono_tz::Asia::Tokyo,
623    )
624}
625
626fn hkex_rules() -> Vec<HolidayRule> {
627    let lny: &'static [(i32, u32, u32)] = &[
628        (2020, 1, 27),
629        (2021, 2, 12),
630        (2022, 2, 1),
631        (2023, 1, 23),
632        (2024, 2, 12),
633        (2025, 1, 29),
634        (2026, 2, 17),
635        (2027, 2, 8),
636        (2028, 1, 26),
637        (2029, 2, 13),
638        (2030, 2, 4),
639    ];
640    vec![
641        fixed(1, 1, None),
642        HolidayRule::Tabulated { table: lny },
643        easter(-2),
644        easter(1),
645        fixed(5, 1, None),
646        fixed(7, 1, None),
647        fixed(10, 1, None),
648        fixed(12, 25, None),
649        fixed(12, 26, None),
650    ]
651}
652
653fn hkex_trading_hours() -> TradingHours {
654    TradingHours::new(
655        NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
656        NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
657        chrono_tz::Asia::Hong_Kong,
658    )
659}
660
661fn sse_rules() -> Vec<HolidayRule> {
662    let lny: &'static [(i32, u32, u32)] = &[
663        (2020, 1, 25),
664        (2021, 2, 12),
665        (2022, 2, 1),
666        (2023, 1, 22),
667        (2024, 2, 10),
668        (2025, 1, 29),
669        (2026, 2, 17),
670        (2027, 2, 6),
671        (2028, 1, 26),
672        (2029, 2, 13),
673        (2030, 2, 3),
674    ];
675    vec![
676        fixed(1, 1, None),
677        HolidayRule::Tabulated { table: lny },
678        fixed(5, 1, None),
679        fixed(10, 1, None),
680        fixed(10, 2, None),
681        fixed(10, 3, None),
682    ]
683}
684
685fn sse_trading_hours() -> TradingHours {
686    TradingHours::new(
687        NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
688        NaiveTime::from_hms_opt(15, 0, 0).unwrap(),
689        chrono_tz::Asia::Shanghai,
690    )
691}
692
693fn xetra_rules() -> Vec<HolidayRule> {
694    vec![
695        fixed(1, 1, None),
696        easter(-2),
697        easter(1),
698        fixed(5, 1, None),
699        fixed(10, 3, None),
700        fixed(12, 24, None),
701        fixed(12, 25, None),
702        fixed(12, 26, None),
703        fixed(12, 31, None),
704    ]
705}
706
707fn xetra_trading_hours() -> TradingHours {
708    TradingHours::new(
709        NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
710        NaiveTime::from_hms_opt(17, 30, 0).unwrap(),
711        chrono_tz::Europe::Berlin,
712    )
713}
714
715fn euronext_paris_rules() -> Vec<HolidayRule> {
716    vec![
717        fixed(1, 1, None),
718        easter(-2),
719        easter(1),
720        fixed(5, 1, None),
721        fixed(12, 25, None),
722        fixed(12, 26, None),
723    ]
724}
725
726fn euronext_paris_trading_hours() -> TradingHours {
727    TradingHours::new(
728        NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
729        NaiveTime::from_hms_opt(17, 30, 0).unwrap(),
730        chrono_tz::Europe::Paris,
731    )
732}
733
734fn tsx_rules() -> Vec<HolidayRule> {
735    vec![
736        fixed(1, 1, None),
737        nth(2, Weekday::Mon, 3),
738        easter(-2),
739        nth(5, Weekday::Mon, -1),
740        fixed(7, 1, None),
741        nth(8, Weekday::Mon, 1),
742        nth(9, Weekday::Mon, 1),
743        nth(10, Weekday::Mon, 2),
744        fixed(12, 25, None),
745        fixed(12, 26, None),
746    ]
747}
748
749fn tsx_trading_hours() -> TradingHours {
750    TradingHours::new(
751        NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
752        NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
753        chrono_tz::America::Toronto,
754    )
755}
756
757fn asx_rules() -> Vec<HolidayRule> {
758    vec![
759        fixed(1, 1, None),
760        fixed(1, 26, None),
761        easter(-2),
762        easter(1),
763        fixed(4, 25, None),
764        nth(6, Weekday::Mon, 2),
765        fixed(12, 25, None),
766        fixed(12, 26, None),
767    ]
768}
769
770fn asx_trading_hours() -> TradingHours {
771    TradingHours::new(
772        NaiveTime::from_hms_opt(10, 0, 0).unwrap(),
773        NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
774        chrono_tz::Australia::Sydney,
775    )
776}
777
778fn nse_rules() -> Vec<HolidayRule> {
779    vec![
780        fixed(1, 26, None),
781        fixed(8, 15, None),
782        fixed(10, 2, None),
783        fixed(12, 25, None),
784    ]
785}
786
787fn nse_trading_hours() -> TradingHours {
788    TradingHours::new(
789        NaiveTime::from_hms_opt(9, 15, 0).unwrap(),
790        NaiveTime::from_hms_opt(15, 30, 0).unwrap(),
791        chrono_tz::Asia::Kolkata,
792    )
793}
794
795// ---------- Early-close rule helpers ----------
796
797fn ec(rule: HolidayRule, h: u32, m: u32) -> EarlyCloseRule {
798    EarlyCloseRule {
799        rule,
800        close_time: NaiveTime::from_hms_opt(h, m, 0).unwrap(),
801    }
802}
803
804/// NYSE/NASDAQ early closes (13:00 ET):
805/// - Day after Thanksgiving (Black Friday)
806/// - Christmas Eve when it falls on a weekday (Dec 24)
807/// - July 3 when it falls on a weekday (day before Independence Day)
808///
809/// These are "best-effort" rules; the SEC/NYSE may publish ad-hoc deviations.
810fn nyse_early_closes() -> Vec<EarlyCloseRule> {
811    // Black Friday is the day after the 4th Thursday of November (i.e.
812    // Thanksgiving + 1). Tabulated through 2035 — easily extended.
813    static BLACK_FRIDAY: &[(i32, u32, u32)] = &[
814        (2020, 11, 27),
815        (2021, 11, 26),
816        (2022, 11, 25),
817        (2023, 11, 24),
818        (2024, 11, 29),
819        (2025, 11, 28),
820        (2026, 11, 27),
821        (2027, 11, 26),
822        (2028, 11, 24),
823        (2029, 11, 23),
824        (2030, 11, 29),
825        (2031, 11, 28),
826        (2032, 11, 26),
827        (2033, 11, 25),
828        (2034, 11, 24),
829        (2035, 11, 23),
830    ];
831    vec![
832        ec(
833            HolidayRule::Tabulated {
834                table: BLACK_FRIDAY,
835            },
836            13,
837            0,
838        ),
839        ec(fixed_no_roll(12, 24, None), 13, 0),
840        ec(fixed_no_roll(7, 3, None), 13, 0),
841    ]
842}
843
844// ---------- Additional non-US equity calendars ----------
845
846/// Generic European Christian-calendar holidays: NY, Good Friday,
847/// Easter Monday, May Day, Christmas, Boxing Day. Used as a baseline.
848fn euro_basic_rules() -> Vec<HolidayRule> {
849    vec![
850        fixed(1, 1, None),
851        easter(-2),
852        easter(1),
853        fixed(5, 1, None),
854        fixed(12, 25, None),
855        fixed(12, 26, None),
856    ]
857}
858
859/// Euronext Amsterdam: same hours as Paris/Brussels/Lisbon (09:00–17:30 CET).
860fn euronext_hours(tz: chrono_tz::Tz) -> TradingHours {
861    TradingHours::new(
862        NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
863        NaiveTime::from_hms_opt(17, 30, 0).unwrap(),
864        tz,
865    )
866}
867
868fn xams_rules() -> Vec<HolidayRule> {
869    // Amsterdam: NY, Good Friday, Easter Mon, King's Day (Apr 27, since 2014),
870    // Ascension (+39), Whit Monday (+50), Christmas, Boxing Day.
871    vec![
872        fixed(1, 1, None),
873        easter(-2),
874        easter(1),
875        fixed_no_roll(4, 27, Some(2014)),
876        easter(39),
877        easter(50),
878        fixed(12, 25, None),
879        fixed(12, 26, None),
880    ]
881}
882
883fn xbru_rules() -> Vec<HolidayRule> {
884    // Brussels: NY, Good Friday, Easter Mon, Labour, Ascension, Whit Mon,
885    // Christmas, Boxing Day.
886    vec![
887        fixed(1, 1, None),
888        easter(-2),
889        easter(1),
890        fixed(5, 1, None),
891        easter(39),
892        easter(50),
893        fixed(12, 25, None),
894        fixed(12, 26, None),
895    ]
896}
897
898fn xlis_rules() -> Vec<HolidayRule> {
899    // Lisbon: subset of euro_basic + Carnival (Easter -47).
900    let mut r = euro_basic_rules();
901    r.push(easter(-47));
902    r
903}
904
905fn xmil_rules() -> Vec<HolidayRule> {
906    // Borsa Italiana (Milan): NY, Epiphany (Jan 6), Easter Mon, Liberation
907    // Day (Apr 25), Labour, Republic Day (Jun 2), Assumption (Aug 15),
908    // All Saints (Nov 1), Immaculate Conception (Dec 8), Christmas, Boxing.
909    vec![
910        fixed(1, 1, None),
911        fixed_no_roll(1, 6, None),
912        easter(-2),
913        easter(1),
914        fixed_no_roll(4, 25, None),
915        fixed(5, 1, None),
916        fixed_no_roll(6, 2, None),
917        fixed_no_roll(8, 15, None),
918        fixed_no_roll(11, 1, None),
919        fixed_no_roll(12, 8, None),
920        fixed(12, 25, None),
921        fixed(12, 26, None),
922    ]
923}
924
925fn xmil_hours() -> TradingHours {
926    TradingHours::new(
927        NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
928        NaiveTime::from_hms_opt(17, 30, 0).unwrap(),
929        chrono_tz::Europe::Rome,
930    )
931}
932
933fn xmad_rules() -> Vec<HolidayRule> {
934    // BME Madrid: NY, Epiphany, Good Friday, Easter Mon, Labour,
935    // Assumption, National Day (Oct 12), All Saints, Constitution (Dec 6),
936    // Immaculate (Dec 8), Christmas, Boxing.
937    vec![
938        fixed(1, 1, None),
939        fixed_no_roll(1, 6, None),
940        easter(-2),
941        easter(1),
942        fixed(5, 1, None),
943        fixed_no_roll(8, 15, None),
944        fixed_no_roll(10, 12, None),
945        fixed_no_roll(11, 1, None),
946        fixed_no_roll(12, 6, None),
947        fixed_no_roll(12, 8, None),
948        fixed(12, 25, None),
949        fixed(12, 26, None),
950    ]
951}
952
953fn xmad_hours() -> TradingHours {
954    TradingHours::new(
955        NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
956        NaiveTime::from_hms_opt(17, 30, 0).unwrap(),
957        chrono_tz::Europe::Madrid,
958    )
959}
960
961fn xswx_rules() -> Vec<HolidayRule> {
962    // SIX Swiss: NY, Berchtold (Jan 2), Good Friday, Easter Mon, Labour,
963    // Ascension, Whit Mon, Swiss National (Aug 1), Christmas, Boxing.
964    vec![
965        fixed(1, 1, None),
966        fixed_no_roll(1, 2, None),
967        easter(-2),
968        easter(1),
969        fixed(5, 1, None),
970        easter(39),
971        easter(50),
972        fixed_no_roll(8, 1, None),
973        fixed(12, 25, None),
974        fixed(12, 26, None),
975    ]
976}
977
978fn xswx_hours() -> TradingHours {
979    TradingHours::new(
980        NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
981        NaiveTime::from_hms_opt(17, 30, 0).unwrap(),
982        chrono_tz::Europe::Zurich,
983    )
984}
985
986fn xosl_rules() -> Vec<HolidayRule> {
987    // Oslo Børs: NY, Maundy Thu (-3), Good Friday, Easter Mon, Labour,
988    // Constitution (May 17), Ascension, Whit Mon, Christmas Eve (half),
989    // Christmas, Boxing, NYE (half).
990    vec![
991        fixed(1, 1, None),
992        easter(-3),
993        easter(-2),
994        easter(1),
995        fixed(5, 1, None),
996        fixed_no_roll(5, 17, None),
997        easter(39),
998        easter(50),
999        fixed(12, 25, None),
1000        fixed(12, 26, None),
1001    ]
1002}
1003
1004fn xosl_hours() -> TradingHours {
1005    TradingHours::new(
1006        NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1007        NaiveTime::from_hms_opt(16, 20, 0).unwrap(),
1008        chrono_tz::Europe::Oslo,
1009    )
1010}
1011
1012fn xsto_rules() -> Vec<HolidayRule> {
1013    // Stockholm OMX: NY, Epiphany, Good Friday, Easter Mon, Labour,
1014    // Ascension, National Day (Jun 6), Midsummer Eve (Fri before Jun 20-26),
1015    // Christmas Eve, Christmas, Boxing, NYE.
1016    vec![
1017        fixed(1, 1, None),
1018        fixed_no_roll(1, 6, None),
1019        easter(-2),
1020        easter(1),
1021        fixed(5, 1, None),
1022        easter(39),
1023        fixed_no_roll(6, 6, None),
1024        fixed_no_roll(12, 24, None),
1025        fixed(12, 25, None),
1026        fixed(12, 26, None),
1027        fixed_no_roll(12, 31, None),
1028    ]
1029}
1030
1031fn xsto_hours() -> TradingHours {
1032    TradingHours::new(
1033        NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1034        NaiveTime::from_hms_opt(17, 30, 0).unwrap(),
1035        chrono_tz::Europe::Stockholm,
1036    )
1037}
1038
1039fn xhel_rules() -> Vec<HolidayRule> {
1040    // Helsinki: NY, Epiphany, Good Friday, Easter Mon, Labour,
1041    // Ascension, Midsummer Eve (skip), Independence Day (Dec 6),
1042    // Christmas Eve, Christmas, Boxing.
1043    vec![
1044        fixed(1, 1, None),
1045        fixed_no_roll(1, 6, None),
1046        easter(-2),
1047        easter(1),
1048        fixed(5, 1, None),
1049        easter(39),
1050        fixed_no_roll(12, 6, None),
1051        fixed_no_roll(12, 24, None),
1052        fixed(12, 25, None),
1053        fixed(12, 26, None),
1054    ]
1055}
1056
1057fn xhel_hours() -> TradingHours {
1058    TradingHours::new(
1059        NaiveTime::from_hms_opt(10, 0, 0).unwrap(),
1060        NaiveTime::from_hms_opt(18, 30, 0).unwrap(),
1061        chrono_tz::Europe::Helsinki,
1062    )
1063}
1064
1065fn xcse_rules() -> Vec<HolidayRule> {
1066    // Copenhagen: NY, Maundy Thu, Good Friday, Easter Mon, Great Prayer Day
1067    // (was Easter+26, abolished 2024), Ascension, Constitution (Jun 5),
1068    // Christmas Eve, Christmas, Boxing.
1069    vec![
1070        fixed(1, 1, None),
1071        easter(-3),
1072        easter(-2),
1073        easter(1),
1074        easter(39),
1075        fixed_no_roll(6, 5, None),
1076        fixed_no_roll(12, 24, None),
1077        fixed(12, 25, None),
1078        fixed(12, 26, None),
1079    ]
1080}
1081
1082fn xcse_hours() -> TradingHours {
1083    TradingHours::new(
1084        NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1085        NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
1086        chrono_tz::Europe::Copenhagen,
1087    )
1088}
1089
1090fn xice_rules() -> Vec<HolidayRule> {
1091    // Iceland: NY, Maundy Thu, Good Fri, Easter Mon, First Day of Summer
1092    // (skip), Labour, Ascension, Whit Mon, National Day (Jun 17),
1093    // Commerce Day (skip), Christmas Eve, Christmas, Boxing.
1094    vec![
1095        fixed(1, 1, None),
1096        easter(-3),
1097        easter(-2),
1098        easter(1),
1099        fixed(5, 1, None),
1100        easter(39),
1101        easter(50),
1102        fixed_no_roll(6, 17, None),
1103        fixed_no_roll(12, 24, None),
1104        fixed(12, 25, None),
1105        fixed(12, 26, None),
1106    ]
1107}
1108
1109fn xice_hours() -> TradingHours {
1110    TradingHours::new(
1111        NaiveTime::from_hms_opt(10, 0, 0).unwrap(),
1112        NaiveTime::from_hms_opt(15, 30, 0).unwrap(),
1113        chrono_tz::Atlantic::Reykjavik,
1114    )
1115}
1116
1117fn xwar_rules() -> Vec<HolidayRule> {
1118    // Warsaw: NY, Epiphany, Easter Mon, Labour, Constitution (May 3),
1119    // Corpus Christi (+60), Assumption, All Saints, Independence (Nov 11),
1120    // Christmas, Boxing.
1121    vec![
1122        fixed(1, 1, None),
1123        fixed_no_roll(1, 6, None),
1124        easter(1),
1125        fixed(5, 1, None),
1126        fixed_no_roll(5, 3, None),
1127        easter(60),
1128        fixed_no_roll(8, 15, None),
1129        fixed_no_roll(11, 1, None),
1130        fixed_no_roll(11, 11, None),
1131        fixed(12, 25, None),
1132        fixed(12, 26, None),
1133    ]
1134}
1135
1136fn xwar_hours() -> TradingHours {
1137    TradingHours::new(
1138        NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1139        NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
1140        chrono_tz::Europe::Warsaw,
1141    )
1142}
1143
1144fn xpra_rules() -> Vec<HolidayRule> {
1145    // Prague: NY, Good Friday, Easter Mon, Labour, Liberation (May 8),
1146    // Ss Cyril & Methodius (Jul 5), Jan Hus (Jul 6), Statehood (Sep 28),
1147    // Independence (Oct 28), Freedom (Nov 17), Christmas Eve, Christmas, Boxing.
1148    vec![
1149        fixed(1, 1, None),
1150        easter(-2),
1151        easter(1),
1152        fixed(5, 1, None),
1153        fixed_no_roll(5, 8, None),
1154        fixed_no_roll(7, 5, None),
1155        fixed_no_roll(7, 6, None),
1156        fixed_no_roll(9, 28, None),
1157        fixed_no_roll(10, 28, None),
1158        fixed_no_roll(11, 17, None),
1159        fixed_no_roll(12, 24, None),
1160        fixed(12, 25, None),
1161        fixed(12, 26, None),
1162    ]
1163}
1164
1165fn xpra_hours() -> TradingHours {
1166    TradingHours::new(
1167        NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1168        NaiveTime::from_hms_opt(16, 25, 0).unwrap(),
1169        chrono_tz::Europe::Prague,
1170    )
1171}
1172
1173fn xbud_rules() -> Vec<HolidayRule> {
1174    // Budapest: NY, 1848 Revolution (Mar 15), Good Friday, Easter Mon,
1175    // Labour, Whit Mon, State Foundation (Aug 20), 1956 Revolution (Oct 23),
1176    // All Saints, Christmas, Boxing.
1177    vec![
1178        fixed(1, 1, None),
1179        fixed_no_roll(3, 15, None),
1180        easter(-2),
1181        easter(1),
1182        fixed(5, 1, None),
1183        easter(50),
1184        fixed_no_roll(8, 20, None),
1185        fixed_no_roll(10, 23, None),
1186        fixed_no_roll(11, 1, None),
1187        fixed(12, 25, None),
1188        fixed(12, 26, None),
1189    ]
1190}
1191
1192fn xbud_hours() -> TradingHours {
1193    TradingHours::new(
1194        NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1195        NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
1196        chrono_tz::Europe::Budapest,
1197    )
1198}
1199
1200fn xwbo_rules() -> Vec<HolidayRule> {
1201    // Vienna (Wiener Börse): NY, Good Friday, Easter Mon, Labour,
1202    // Ascension, Whit Mon, Corpus Christi, Assumption, National (Oct 26),
1203    // All Saints, Immaculate (Dec 8), Christmas Eve, Christmas, Boxing.
1204    vec![
1205        fixed(1, 1, None),
1206        easter(-2),
1207        easter(1),
1208        fixed(5, 1, None),
1209        easter(39),
1210        easter(50),
1211        easter(60),
1212        fixed_no_roll(8, 15, None),
1213        fixed_no_roll(10, 26, None),
1214        fixed_no_roll(11, 1, None),
1215        fixed_no_roll(12, 8, None),
1216        fixed_no_roll(12, 24, None),
1217        fixed(12, 25, None),
1218        fixed(12, 26, None),
1219    ]
1220}
1221
1222fn xwbo_hours() -> TradingHours {
1223    TradingHours::new(
1224        NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1225        NaiveTime::from_hms_opt(17, 30, 0).unwrap(),
1226        chrono_tz::Europe::Vienna,
1227    )
1228}
1229
1230fn xdub_rules() -> Vec<HolidayRule> {
1231    // Euronext Dublin: NY, Saint Patrick (Mar 17), Good Friday, Easter Mon,
1232    // May Day (1st Mon), June Bank (1st Mon), August Bank (1st Mon),
1233    // October Bank (last Mon), Christmas, Boxing.
1234    vec![
1235        fixed(1, 1, None),
1236        fixed(3, 17, None),
1237        easter(-2),
1238        easter(1),
1239        nth(5, Weekday::Mon, 1),
1240        nth(6, Weekday::Mon, 1),
1241        nth(8, Weekday::Mon, 1),
1242        nth(10, Weekday::Mon, -1),
1243        fixed(12, 25, None),
1244        fixed(12, 26, None),
1245    ]
1246}
1247
1248fn xdub_hours() -> TradingHours {
1249    TradingHours::new(
1250        NaiveTime::from_hms_opt(8, 0, 0).unwrap(),
1251        NaiveTime::from_hms_opt(16, 28, 0).unwrap(),
1252        chrono_tz::Europe::Dublin,
1253    )
1254}
1255
1256// ---------- Asia / Pacific ----------
1257
1258fn xkrx_rules() -> Vec<HolidayRule> {
1259    // Korea Exchange: tabulated lunar holidays (Seollal, Chuseok). For
1260    // accuracy these are baked in as lookup tables 2020-2030.
1261    let seollal: &'static [(i32, u32, u32)] = &[
1262        (2020, 1, 24),
1263        (2020, 1, 27),
1264        (2021, 2, 11),
1265        (2021, 2, 12),
1266        (2022, 1, 31),
1267        (2022, 2, 1),
1268        (2022, 2, 2),
1269        (2023, 1, 23),
1270        (2023, 1, 24),
1271        (2024, 2, 9),
1272        (2024, 2, 12),
1273        (2025, 1, 28),
1274        (2025, 1, 29),
1275        (2025, 1, 30),
1276        (2026, 2, 16),
1277        (2026, 2, 17),
1278        (2026, 2, 18),
1279    ];
1280    let chuseok: &'static [(i32, u32, u32)] = &[
1281        (2020, 9, 30),
1282        (2020, 10, 1),
1283        (2020, 10, 2),
1284        (2021, 9, 20),
1285        (2021, 9, 21),
1286        (2021, 9, 22),
1287        (2022, 9, 9),
1288        (2022, 9, 12),
1289        (2023, 9, 28),
1290        (2023, 9, 29),
1291        (2024, 9, 16),
1292        (2024, 9, 17),
1293        (2024, 9, 18),
1294        (2025, 10, 6),
1295        (2025, 10, 7),
1296        (2025, 10, 8),
1297        (2026, 9, 24),
1298        (2026, 9, 25),
1299    ];
1300    vec![
1301        fixed(1, 1, None),
1302        HolidayRule::Tabulated { table: seollal },
1303        fixed_no_roll(3, 1, None),  // Independence Movement
1304        fixed_no_roll(5, 5, None),  // Children's Day
1305        fixed_no_roll(6, 6, None),  // Memorial Day
1306        fixed_no_roll(8, 15, None), // Liberation Day
1307        HolidayRule::Tabulated { table: chuseok },
1308        fixed_no_roll(10, 3, None), // National Foundation
1309        fixed_no_roll(10, 9, None), // Hangul Day
1310        fixed(12, 25, None),
1311    ]
1312}
1313
1314fn xkrx_hours() -> TradingHours {
1315    TradingHours::new(
1316        NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1317        NaiveTime::from_hms_opt(15, 30, 0).unwrap(),
1318        chrono_tz::Asia::Seoul,
1319    )
1320}
1321
1322fn xses_rules() -> Vec<HolidayRule> {
1323    // Singapore Exchange: NY, Lunar NY (use Shanghai's table), Good Friday,
1324    // Labour, Vesak Day (varies), National Day (Aug 9), Christmas. Vesak
1325    // and others use simplified handling.
1326    let lny: &'static [(i32, u32, u32)] = &[
1327        (2020, 1, 24),
1328        (2021, 2, 12),
1329        (2022, 2, 1),
1330        (2023, 1, 23),
1331        (2024, 2, 12),
1332        (2025, 1, 29),
1333        (2026, 2, 17),
1334    ];
1335    let lny2: &'static [(i32, u32, u32)] = &[
1336        (2020, 1, 27),
1337        (2021, 2, 15),
1338        (2022, 2, 2),
1339        (2023, 1, 24),
1340        (2024, 2, 13),
1341        (2025, 1, 30),
1342        (2026, 2, 18),
1343    ];
1344    vec![
1345        fixed(1, 1, None),
1346        HolidayRule::Tabulated { table: lny },
1347        HolidayRule::Tabulated { table: lny2 },
1348        easter(-2),
1349        fixed(5, 1, None),
1350        fixed(8, 9, None),
1351        fixed(12, 25, None),
1352    ]
1353}
1354
1355fn xses_hours() -> TradingHours {
1356    TradingHours::new(
1357        NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1358        NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
1359        chrono_tz::Asia::Singapore,
1360    )
1361}
1362
1363fn xtai_rules() -> Vec<HolidayRule> {
1364    // Taiwan Stock Exchange: tabulated Lunar NY (5-7 day closure), Children's
1365    // Day (Apr 4), Tomb Sweeping (Apr 5), Dragon Boat, Mid-Autumn, ROC
1366    // National (Oct 10).
1367    let lny: &'static [(i32, u32, u32)] = &[
1368        (2020, 1, 23),
1369        (2021, 2, 8),
1370        (2022, 1, 27),
1371        (2023, 1, 19),
1372        (2024, 2, 5),
1373        (2025, 1, 23),
1374        (2026, 2, 13),
1375    ];
1376    vec![
1377        fixed(1, 1, None),
1378        HolidayRule::Tabulated { table: lny },
1379        fixed_no_roll(2, 28, None), // Peace Memorial
1380        fixed_no_roll(4, 4, None),  // Children's
1381        fixed_no_roll(4, 5, None),  // Tomb Sweeping
1382        fixed(5, 1, None),
1383        fixed_no_roll(10, 10, None), // ROC National
1384    ]
1385}
1386
1387fn xtai_hours() -> TradingHours {
1388    TradingHours::new(
1389        NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1390        NaiveTime::from_hms_opt(13, 30, 0).unwrap(),
1391        chrono_tz::Asia::Taipei,
1392    )
1393}
1394
1395fn xbkk_rules() -> Vec<HolidayRule> {
1396    // SET Bangkok: NY, Chakri Day (Apr 6), Songkran (Apr 13-15), Labour,
1397    // Coronation (May 4), Visakha (varies), Asanha (varies), Queen's
1398    // Birthday (Aug 12), King Bhumibol Memorial (Oct 13), Chulalongkorn
1399    // (Oct 23), King's Birthday (Dec 5), Constitution (Dec 10), NYE.
1400    vec![
1401        fixed(1, 1, None),
1402        fixed(4, 6, None),
1403        fixed_no_roll(4, 13, None),
1404        fixed_no_roll(4, 14, None),
1405        fixed_no_roll(4, 15, None),
1406        fixed(5, 1, None),
1407        fixed_no_roll(5, 4, None),
1408        fixed_no_roll(8, 12, None),
1409        fixed(10, 13, None),
1410        fixed(10, 23, None),
1411        fixed(12, 5, None),
1412        fixed(12, 10, None),
1413        fixed_no_roll(12, 31, None),
1414    ]
1415}
1416
1417fn xbkk_hours() -> TradingHours {
1418    TradingHours::new(
1419        NaiveTime::from_hms_opt(10, 0, 0).unwrap(),
1420        NaiveTime::from_hms_opt(16, 30, 0).unwrap(),
1421        chrono_tz::Asia::Bangkok,
1422    )
1423}
1424
1425fn xkls_rules() -> Vec<HolidayRule> {
1426    // Bursa Malaysia: NY, Lunar NY, Labour, Wesak, Yang di-Pertuan
1427    // Agong's Birthday (1st Mon Jun), National (Aug 31), Malaysia Day
1428    // (Sep 16), Christmas. Eid/Hari Raya are tabulated.
1429    let lny: &'static [(i32, u32, u32)] = &[
1430        (2020, 1, 27),
1431        (2021, 2, 12),
1432        (2022, 2, 1),
1433        (2023, 1, 23),
1434        (2024, 2, 12),
1435        (2025, 1, 29),
1436        (2026, 2, 17),
1437    ];
1438    vec![
1439        fixed(1, 1, None),
1440        HolidayRule::Tabulated { table: lny },
1441        fixed(5, 1, None),
1442        nth(6, Weekday::Mon, 1),
1443        fixed(8, 31, None),
1444        fixed(9, 16, None),
1445        fixed(12, 25, None),
1446    ]
1447}
1448
1449fn xkls_hours() -> TradingHours {
1450    TradingHours::new(
1451        NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1452        NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
1453        chrono_tz::Asia::Kuala_Lumpur,
1454    )
1455}
1456
1457fn xidx_rules() -> Vec<HolidayRule> {
1458    // Indonesia: NY, Lunar NY, Labour, Pancasila (Jun 1), Independence
1459    // (Aug 17), Christmas. Religious dates simplified.
1460    let lny: &'static [(i32, u32, u32)] = &[
1461        (2020, 1, 27),
1462        (2021, 2, 12),
1463        (2022, 2, 1),
1464        (2023, 1, 23),
1465        (2024, 2, 8),
1466        (2025, 1, 29),
1467        (2026, 2, 17),
1468    ];
1469    vec![
1470        fixed(1, 1, None),
1471        HolidayRule::Tabulated { table: lny },
1472        fixed(5, 1, None),
1473        fixed(6, 1, None),
1474        fixed(8, 17, None),
1475        fixed(12, 25, None),
1476    ]
1477}
1478
1479fn xidx_hours() -> TradingHours {
1480    TradingHours::new(
1481        NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1482        NaiveTime::from_hms_opt(15, 50, 0).unwrap(),
1483        chrono_tz::Asia::Jakarta,
1484    )
1485}
1486
1487fn xphs_rules() -> Vec<HolidayRule> {
1488    // Philippine Stock Exchange: NY, Maundy Thu, Good Fri, Araw ng Kagitingan
1489    // (Apr 9), Labour, Independence (Jun 12), Ninoy Aquino (Aug 21), National
1490    // Heroes (last Mon Aug), All Saints, Bonifacio (Nov 30), Christmas,
1491    // Rizal Day (Dec 30), NYE.
1492    vec![
1493        fixed(1, 1, None),
1494        easter(-3),
1495        easter(-2),
1496        fixed(4, 9, None),
1497        fixed(5, 1, None),
1498        fixed(6, 12, None),
1499        fixed(8, 21, None),
1500        nth(8, Weekday::Mon, -1),
1501        fixed_no_roll(11, 1, None),
1502        fixed(11, 30, None),
1503        fixed(12, 25, None),
1504        fixed(12, 30, None),
1505        fixed_no_roll(12, 31, None),
1506    ]
1507}
1508
1509fn xphs_hours() -> TradingHours {
1510    TradingHours::new(
1511        NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
1512        NaiveTime::from_hms_opt(15, 0, 0).unwrap(),
1513        chrono_tz::Asia::Manila,
1514    )
1515}
1516
1517fn xnze_rules() -> Vec<HolidayRule> {
1518    // NZX (New Zealand): NY (Jan 1, Jan 2 observed), Waitangi (Feb 6),
1519    // Good Fri, Easter Mon, ANZAC (Apr 25), King's Birthday (1st Mon Jun),
1520    // Matariki (variable, skipped here), Labour Day (4th Mon Oct),
1521    // Christmas, Boxing Day.
1522    vec![
1523        fixed(1, 1, None),
1524        fixed(1, 2, None),
1525        fixed(2, 6, None),
1526        easter(-2),
1527        easter(1),
1528        fixed(4, 25, None),
1529        nth(6, Weekday::Mon, 1),
1530        nth(10, Weekday::Mon, 4),
1531        fixed(12, 25, None),
1532        fixed(12, 26, None),
1533    ]
1534}
1535
1536fn xnze_hours() -> TradingHours {
1537    TradingHours::new(
1538        NaiveTime::from_hms_opt(10, 0, 0).unwrap(),
1539        NaiveTime::from_hms_opt(16, 45, 0).unwrap(),
1540        chrono_tz::Pacific::Auckland,
1541    )
1542}
1543
1544// ---------- EMEA ----------
1545
1546fn xjse_rules() -> Vec<HolidayRule> {
1547    // Johannesburg: NY, Human Rights (Mar 21), Good Fri, Family Day (Easter
1548    // Mon), Freedom (Apr 27), Workers (May 1), Youth (Jun 16), National
1549    // Women's (Aug 9), Heritage (Sep 24), Day of Reconciliation (Dec 16),
1550    // Christmas, Day of Goodwill (Dec 26).
1551    vec![
1552        fixed(1, 1, None),
1553        fixed(3, 21, None),
1554        easter(-2),
1555        easter(1),
1556        fixed(4, 27, None),
1557        fixed(5, 1, None),
1558        fixed(6, 16, None),
1559        fixed(8, 9, None),
1560        fixed(9, 24, None),
1561        fixed(12, 16, None),
1562        fixed(12, 25, None),
1563        fixed(12, 26, None),
1564    ]
1565}
1566
1567fn xjse_hours() -> TradingHours {
1568    TradingHours::new(
1569        NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
1570        NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
1571        chrono_tz::Africa::Johannesburg,
1572    )
1573}
1574
1575/// Sun-Thu weekmask used by Saudi/Gulf venues. Mon=0, Sun=6.
1576const MIDEAST_WEEKMASK: [bool; 7] = [true, true, true, true, false, false, true];
1577
1578fn xsau_rules() -> Vec<HolidayRule> {
1579    // Saudi Tadawul: National Day (Sep 23), Founding Day (Feb 22). Eid
1580    // dates vary by lunar calendar — kept tabulated for accuracy 2020-2026.
1581    let eid_fitr: &'static [(i32, u32, u32)] = &[
1582        (2020, 5, 24),
1583        (2021, 5, 13),
1584        (2022, 5, 2),
1585        (2023, 4, 21),
1586        (2024, 4, 10),
1587        (2025, 3, 30),
1588        (2026, 3, 20),
1589    ];
1590    let eid_adha: &'static [(i32, u32, u32)] = &[
1591        (2020, 7, 31),
1592        (2021, 7, 20),
1593        (2022, 7, 9),
1594        (2023, 6, 28),
1595        (2024, 6, 16),
1596        (2025, 6, 6),
1597        (2026, 5, 27),
1598    ];
1599    vec![
1600        fixed_no_roll(2, 22, Some(2022)),
1601        fixed_no_roll(9, 23, None),
1602        HolidayRule::Tabulated { table: eid_fitr },
1603        HolidayRule::Tabulated { table: eid_adha },
1604    ]
1605}
1606
1607fn xsau_hours() -> TradingHours {
1608    TradingHours::new(
1609        NaiveTime::from_hms_opt(10, 0, 0).unwrap(),
1610        NaiveTime::from_hms_opt(15, 0, 0).unwrap(),
1611        chrono_tz::Asia::Riyadh,
1612    )
1613}
1614
1615fn xist_rules() -> Vec<HolidayRule> {
1616    // Borsa Istanbul: NY, National Sovereignty (Apr 23), Labour (May 1),
1617    // Commemoration of Atatürk (May 19), Democracy (Jul 15), Victory (Aug 30),
1618    // Republic (Oct 29). Eid dates vary; tabulated.
1619    let eid_fitr: &'static [(i32, u32, u32)] = &[
1620        (2020, 5, 24),
1621        (2021, 5, 13),
1622        (2022, 5, 2),
1623        (2023, 4, 21),
1624        (2024, 4, 10),
1625        (2025, 3, 30),
1626        (2026, 3, 20),
1627    ];
1628    let eid_adha: &'static [(i32, u32, u32)] = &[
1629        (2020, 7, 31),
1630        (2021, 7, 20),
1631        (2022, 7, 9),
1632        (2023, 6, 28),
1633        (2024, 6, 16),
1634        (2025, 6, 6),
1635        (2026, 5, 27),
1636    ];
1637    vec![
1638        fixed(1, 1, None),
1639        fixed(4, 23, None),
1640        fixed(5, 1, None),
1641        fixed(5, 19, None),
1642        fixed(7, 15, None),
1643        fixed(8, 30, None),
1644        fixed(10, 29, None),
1645        HolidayRule::Tabulated { table: eid_fitr },
1646        HolidayRule::Tabulated { table: eid_adha },
1647    ]
1648}
1649
1650fn xist_hours() -> TradingHours {
1651    TradingHours::new(
1652        NaiveTime::from_hms_opt(10, 0, 0).unwrap(),
1653        NaiveTime::from_hms_opt(18, 0, 0).unwrap(),
1654        chrono_tz::Europe::Istanbul,
1655    )
1656}
1657
1658/// Sun-Thu weekmask used by TASE.
1659const TASE_WEEKMASK: [bool; 7] = [true, true, true, true, false, false, true];
1660
1661fn xtae_rules() -> Vec<HolidayRule> {
1662    // Tel Aviv Stock Exchange: tabulated Jewish holidays (Passover, Shavuot,
1663    // Rosh Hashanah, Yom Kippur, Sukkot, Simchat Torah, Independence Day).
1664    // Tabulated 2020-2026.
1665    let purim: &'static [(i32, u32, u32)] = &[
1666        (2020, 3, 10),
1667        (2021, 2, 26),
1668        (2022, 3, 17),
1669        (2023, 3, 7),
1670        (2024, 3, 24),
1671        (2025, 3, 14),
1672        (2026, 3, 3),
1673    ];
1674    let passover_eve: &'static [(i32, u32, u32)] = &[
1675        (2020, 4, 8),
1676        (2021, 3, 27),
1677        (2022, 4, 15),
1678        (2023, 4, 5),
1679        (2024, 4, 22),
1680        (2025, 4, 12),
1681        (2026, 4, 1),
1682    ];
1683    let shavuot: &'static [(i32, u32, u32)] = &[
1684        (2020, 5, 29),
1685        (2021, 5, 17),
1686        (2022, 6, 5),
1687        (2023, 5, 26),
1688        (2024, 6, 12),
1689        (2025, 6, 2),
1690        (2026, 5, 22),
1691    ];
1692    let rosh: &'static [(i32, u32, u32)] = &[
1693        (2020, 9, 19),
1694        (2021, 9, 7),
1695        (2022, 9, 26),
1696        (2023, 9, 16),
1697        (2024, 10, 3),
1698        (2025, 9, 23),
1699        (2026, 9, 12),
1700    ];
1701    let yom_kippur: &'static [(i32, u32, u32)] = &[
1702        (2020, 9, 28),
1703        (2021, 9, 16),
1704        (2022, 10, 5),
1705        (2023, 9, 25),
1706        (2024, 10, 12),
1707        (2025, 10, 2),
1708        (2026, 9, 21),
1709    ];
1710    let sukkot: &'static [(i32, u32, u32)] = &[
1711        (2020, 10, 3),
1712        (2021, 9, 21),
1713        (2022, 10, 10),
1714        (2023, 9, 30),
1715        (2024, 10, 17),
1716        (2025, 10, 7),
1717        (2026, 9, 26),
1718    ];
1719    let independence: &'static [(i32, u32, u32)] = &[
1720        (2020, 4, 29),
1721        (2021, 4, 15),
1722        (2022, 5, 5),
1723        (2023, 4, 26),
1724        (2024, 5, 14),
1725        (2025, 5, 1),
1726        (2026, 4, 22),
1727    ];
1728    vec![
1729        HolidayRule::Tabulated { table: purim },
1730        HolidayRule::Tabulated {
1731            table: passover_eve,
1732        },
1733        HolidayRule::Tabulated { table: shavuot },
1734        HolidayRule::Tabulated {
1735            table: independence,
1736        },
1737        HolidayRule::Tabulated { table: rosh },
1738        HolidayRule::Tabulated { table: yom_kippur },
1739        HolidayRule::Tabulated { table: sukkot },
1740    ]
1741}
1742
1743fn xtae_hours() -> TradingHours {
1744    TradingHours::new(
1745        NaiveTime::from_hms_opt(9, 59, 0).unwrap(),
1746        NaiveTime::from_hms_opt(17, 14, 0).unwrap(),
1747        chrono_tz::Asia::Jerusalem,
1748    )
1749}
1750
1751fn xdfm_rules() -> Vec<HolidayRule> {
1752    // Dubai Financial Market / ADX: NY, UAE National (Dec 2-3),
1753    // Commemoration (Nov 30). Eid tabulated.
1754    let eid_fitr: &'static [(i32, u32, u32)] = &[
1755        (2020, 5, 24),
1756        (2021, 5, 13),
1757        (2022, 5, 2),
1758        (2023, 4, 21),
1759        (2024, 4, 10),
1760        (2025, 3, 30),
1761        (2026, 3, 20),
1762    ];
1763    let eid_adha: &'static [(i32, u32, u32)] = &[
1764        (2020, 7, 31),
1765        (2021, 7, 20),
1766        (2022, 7, 9),
1767        (2023, 6, 28),
1768        (2024, 6, 16),
1769        (2025, 6, 6),
1770        (2026, 5, 27),
1771    ];
1772    vec![
1773        fixed(1, 1, None),
1774        fixed(11, 30, None),
1775        fixed(12, 2, None),
1776        fixed(12, 3, None),
1777        HolidayRule::Tabulated { table: eid_fitr },
1778        HolidayRule::Tabulated { table: eid_adha },
1779    ]
1780}
1781
1782fn xdfm_hours() -> TradingHours {
1783    TradingHours::new(
1784        NaiveTime::from_hms_opt(10, 0, 0).unwrap(),
1785        NaiveTime::from_hms_opt(15, 0, 0).unwrap(),
1786        chrono_tz::Asia::Dubai,
1787    )
1788}
1789
1790// ---------- LatAm ----------
1791
1792fn bvmf_rules() -> Vec<HolidayRule> {
1793    // B3 / BMF Bovespa (São Paulo): NY, Carnival Mon (-48), Carnival Tue (-47),
1794    // Good Friday, Tiradentes (Apr 21), Labour, Corpus Christi (+60),
1795    // Independence (Sep 7), Our Lady of Aparecida (Oct 12), All Souls (Nov 2),
1796    // Republic (Nov 15), Black Awareness (Nov 20), Christmas Eve, Christmas, NYE.
1797    vec![
1798        fixed(1, 1, None),
1799        easter(-48),
1800        easter(-47),
1801        easter(-2),
1802        fixed(4, 21, None),
1803        fixed(5, 1, None),
1804        easter(60),
1805        fixed(9, 7, None),
1806        fixed(10, 12, None),
1807        fixed(11, 2, None),
1808        fixed(11, 15, None),
1809        fixed(11, 20, Some(2024)),
1810        fixed_no_roll(12, 24, None),
1811        fixed(12, 25, None),
1812        fixed_no_roll(12, 31, None),
1813    ]
1814}
1815
1816fn bvmf_hours() -> TradingHours {
1817    TradingHours::new(
1818        NaiveTime::from_hms_opt(10, 0, 0).unwrap(),
1819        NaiveTime::from_hms_opt(17, 30, 0).unwrap(),
1820        chrono_tz::America::Sao_Paulo,
1821    )
1822}
1823
1824fn xmex_rules() -> Vec<HolidayRule> {
1825    // BMV Mexico: NY, Constitution (1st Mon Feb), Benito Juárez (3rd Mon Mar),
1826    // Maundy Thu, Good Fri, Labour, Independence (Sep 16), Revolution
1827    // (3rd Mon Nov), Christmas.
1828    vec![
1829        fixed(1, 1, None),
1830        nth(2, Weekday::Mon, 1),
1831        nth(3, Weekday::Mon, 3),
1832        easter(-3),
1833        easter(-2),
1834        fixed(5, 1, None),
1835        fixed(9, 16, None),
1836        nth(11, Weekday::Mon, 3),
1837        fixed(12, 25, None),
1838    ]
1839}
1840
1841fn xmex_hours() -> TradingHours {
1842    TradingHours::new(
1843        NaiveTime::from_hms_opt(8, 30, 0).unwrap(),
1844        NaiveTime::from_hms_opt(15, 0, 0).unwrap(),
1845        chrono_tz::America::Mexico_City,
1846    )
1847}
1848
1849fn xbue_rules() -> Vec<HolidayRule> {
1850    // Buenos Aires (BYMA): NY, Carnival Mon/Tue, Truth & Justice (Mar 24),
1851    // Malvinas Day (Apr 2), Good Fri, Labour, May Revolution (May 25),
1852    // Flag Day (Jun 20), Independence (Jul 9), San Martín (3rd Mon Aug),
1853    // Diversity (Oct 12), Sovereignty (Nov 20), Immaculate, Christmas.
1854    vec![
1855        fixed(1, 1, None),
1856        easter(-48),
1857        easter(-47),
1858        fixed(3, 24, None),
1859        fixed(4, 2, None),
1860        easter(-2),
1861        fixed(5, 1, None),
1862        fixed(5, 25, None),
1863        fixed(6, 20, None),
1864        fixed(7, 9, None),
1865        nth(8, Weekday::Mon, 3),
1866        fixed(10, 12, None),
1867        fixed(11, 20, None),
1868        fixed(12, 8, None),
1869        fixed(12, 25, None),
1870    ]
1871}
1872
1873fn xbue_hours() -> TradingHours {
1874    TradingHours::new(
1875        NaiveTime::from_hms_opt(11, 0, 0).unwrap(),
1876        NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
1877        chrono_tz::America::Argentina::Buenos_Aires,
1878    )
1879}
1880
1881fn xsgo_rules() -> Vec<HolidayRule> {
1882    // Santiago Stock Exchange: NY, Good Fri, Holy Sat, Labour, Navy Day
1883    // (May 21), Saint Peter & Paul (Jun 29), Virgen del Carmen (Jul 16),
1884    // Assumption, Independence (Sep 18-19), Columbus (Oct 12), Reformation
1885    // (Oct 31), All Saints, Immaculate, Christmas.
1886    vec![
1887        fixed(1, 1, None),
1888        easter(-2),
1889        easter(-1),
1890        fixed(5, 1, None),
1891        fixed(5, 21, None),
1892        fixed(6, 29, None),
1893        fixed(7, 16, None),
1894        fixed(8, 15, None),
1895        fixed(9, 18, None),
1896        fixed(9, 19, None),
1897        fixed(10, 12, None),
1898        fixed(10, 31, None),
1899        fixed(11, 1, None),
1900        fixed(12, 8, None),
1901        fixed(12, 25, None),
1902    ]
1903}
1904
1905fn xsgo_hours() -> TradingHours {
1906    TradingHours::new(
1907        NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
1908        NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
1909        chrono_tz::America::Santiago,
1910    )
1911}
1912
1913fn xlim_rules() -> Vec<HolidayRule> {
1914    // Lima Stock Exchange: NY, Maundy Thu, Good Fri, Labour, Saint Peter
1915    // & Paul, Independence (Jul 28-29), Santa Rosa (Aug 30), Battle of
1916    // Angamos (Oct 8), All Saints, Immaculate, Christmas.
1917    vec![
1918        fixed(1, 1, None),
1919        easter(-3),
1920        easter(-2),
1921        fixed(5, 1, None),
1922        fixed(6, 29, None),
1923        fixed(7, 28, None),
1924        fixed(7, 29, None),
1925        fixed(8, 30, None),
1926        fixed(10, 8, None),
1927        fixed(11, 1, None),
1928        fixed(12, 8, None),
1929        fixed(12, 25, None),
1930    ]
1931}
1932
1933fn xlim_hours() -> TradingHours {
1934    TradingHours::new(
1935        NaiveTime::from_hms_opt(8, 30, 0).unwrap(),
1936        NaiveTime::from_hms_opt(15, 0, 0).unwrap(),
1937        chrono_tz::America::Lima,
1938    )
1939}
1940
1941fn xbog_rules() -> Vec<HolidayRule> {
1942    // Bogotá Stock Exchange (BVC): NY, Epiphany, Saint Joseph, Maundy Thu,
1943    // Good Fri, Labour, Ascension, Corpus Christi, Sacred Heart, Saint
1944    // Peter & Paul, Independence (Jul 20), Battle of Boyacá (Aug 7),
1945    // Assumption, Race Day (Oct 12), All Saints, Independence of Cartagena
1946    // (Nov 11), Immaculate, Christmas.
1947    vec![
1948        fixed(1, 1, None),
1949        fixed(1, 6, None),
1950        fixed(3, 19, None),
1951        easter(-3),
1952        easter(-2),
1953        fixed(5, 1, None),
1954        easter(39),
1955        easter(60),
1956        easter(68),
1957        fixed(7, 20, None),
1958        fixed(8, 7, None),
1959        fixed(8, 15, None),
1960        fixed(10, 12, None),
1961        fixed(11, 1, None),
1962        fixed(11, 11, None),
1963        fixed(12, 8, None),
1964        fixed(12, 25, None),
1965    ]
1966}
1967
1968fn xbog_hours() -> TradingHours {
1969    TradingHours::new(
1970        NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
1971        NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
1972        chrono_tz::America::Bogota,
1973    )
1974}
1975
1976// ---------- Calendar family resolver ----------
1977
1978/// Logical calendar family. Many MICs share a family.
1979#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1980enum Family {
1981    UsEquity,
1982    UsOptions,
1983    UsBondSifma,
1984    UsFuturesCme,
1985    UsFuturesCmeEnergy,
1986    UsFuturesIce,
1987    UsFuturesCfe,
1988    Forex24x5,
1989    Crypto24x7,
1990    Lse,
1991    Tse,
1992    Hkex,
1993    Sse,
1994    Xetra,
1995    EuronextParis,
1996    EuronextAms,
1997    EuronextBru,
1998    EuronextLis,
1999    EuronextDub,
2000    Tsx,
2001    Asx,
2002    Nse,
2003    Xmil,
2004    Xmad,
2005    Xswx,
2006    Xosl,
2007    Xsto,
2008    Xhel,
2009    Xcse,
2010    Xice,
2011    Xwar,
2012    Xpra,
2013    Xbud,
2014    Xwbo,
2015    Xkrx,
2016    Xses,
2017    Xtai,
2018    Xbkk,
2019    Xkls,
2020    Xidx,
2021    Xphs,
2022    Xnze,
2023    Xjse,
2024    Xsau,
2025    Xist,
2026    Xtae,
2027    Xdfm,
2028    Bvmf,
2029    Xmex,
2030    Xbue,
2031    Xsgo,
2032    Xlim,
2033    Xbog,
2034}
2035
2036fn family_for_mic(mic: &str) -> Option<Family> {
2037    use Family::*;
2038    let m = match mic {
2039        // US equities
2040        "XNYS" | "NYSD" | "XCIS" | "CISD" | "XCHI" | "ARCX" | "ARCD" | "ARCO"
2041        | "XASE" | "AMXO" | "XNAS" | "XNGS" | "XNCM" | "XNMS" | "NASD" | "XNDQ"
2042        | "XBOS" | "BOSD" | "XBXO" | "XPHL" | "XPSX" | "PSXD" | "XPHO" | "XPBT"
2043        | "XPOR" | "XNFI" | "EDGA" | "EDGD" | "EDGX" | "EDDP" | "EDGO" | "BATS"
2044        | "BZXD" | "BATO" | "BATY" | "BYXD" | "MEMX" | "MEMD" | "IEXG" | "LTSE"
2045        | "MIHI" | "MPRL" | "EPRL" | "EPRD" | "XMIO" | "EMLD"
2046        // OTC / FINRA — share the NYSE holiday calendar
2047        | "OTCM" | "CAVE" | "OTCB" | "OTCQ" | "PINL" | "PINI" | "PINX" | "PSGM"
2048        | "PINC" | "FINR" | "FINN" | "FINC" | "FINY" | "XADF" | "FINO" | "OOTC"
2049        // Synthetic / placeholder venues
2050        | "XXXX" | "PYPR" | "SIMU" => UsEquity,
2051        // US options
2052        "XISE" | "GMNI" | "MCRY" | "XCBO" | "C2OX" | "MXOP" | "OPRA" => UsOptions,
2053        // US futures: CME group equity-index/FX/financials
2054        "XCME" | "FCME" | "GLBX" | "XCBT" | "FCBT" | "XKBT" => UsFuturesCme,
2055        // NYMEX (energy/metals) lives under CME group too but with energy hours
2056        "XNYM" => UsFuturesCmeEnergy,
2057        // CFE / ICE / SIFMA / FX / Crypto generic families
2058        "CFE" => UsFuturesCfe,
2059        "ICE_US" => UsFuturesIce,
2060        "SIFMA_US" => UsBondSifma,
2061        "FOREX" => Forex24x5,
2062        "CRYPTO" => Crypto24x7,
2063        // Canada
2064        "XTSE" | "XDRK" | "VDRK" | "XTSX" | "XTNX" | "XATS" | "XATX" | "ADRK"
2065        | "XMOD" | "XMOC" | "NEOE" | "NEOD" | "NEON" | "NEOC" | "XCNQ" | "PURE"
2066        | "CSE2" => Tsx,
2067        // Major non-US equities
2068        "XLON" => Lse,
2069        "XTKS" => Tse,
2070        "XHKG" => Hkex,
2071        "XSHG" => Sse,
2072        "XEUR" | "XFRA" => Xetra,
2073        "XPAR" => EuronextParis,
2074        "XAMS" => EuronextAms,
2075        "XBRU" => EuronextBru,
2076        "XLIS" => EuronextLis,
2077        "XDUB" => EuronextDub,
2078        "XMIL" => Xmil,
2079        "XMAD" => Xmad,
2080        "XSWX" => Xswx,
2081        "XOSL" => Xosl,
2082        "XSTO" => Xsto,
2083        "XHEL" => Xhel,
2084        "XCSE" => Xcse,
2085        "XICE" => Xice,
2086        "XWAR" => Xwar,
2087        "XPRA" => Xpra,
2088        "XBUD" => Xbud,
2089        "XWBO" => Xwbo,
2090        "XASX" => Asx,
2091        "XBOM" | "XNSE" => Nse,
2092        "XKRX" => Xkrx,
2093        "XSES" => Xses,
2094        "XTAI" => Xtai,
2095        "XBKK" => Xbkk,
2096        "XKLS" => Xkls,
2097        "XIDX" => Xidx,
2098        "XPHS" => Xphs,
2099        "XNZE" => Xnze,
2100        "XJSE" => Xjse,
2101        "XSAU" => Xsau,
2102        "XIST" => Xist,
2103        "XTAE" => Xtae,
2104        "XDFM" | "XADS" => Xdfm,
2105        "BVMF" => Bvmf,
2106        "XMEX" => Xmex,
2107        "XBUE" => Xbue,
2108        "XSGO" => Xsgo,
2109        "XLIM" => Xlim,
2110        "XBOG" => Xbog,
2111        _ => return None,
2112    };
2113    Some(m)
2114}
2115
2116fn build_family(name: &str, fam: Family) -> Calendar {
2117    use Family::*;
2118    match fam {
2119        UsEquity => Calendar::with_type(
2120            name,
2121            MarketType::Equity,
2122            STANDARD_WEEKMASK,
2123            nyse_rules(),
2124            Some(nyse_trading_hours()),
2125        )
2126        .with_early_closes(nyse_early_closes()),
2127        UsOptions => Calendar::with_type(
2128            name,
2129            MarketType::Options,
2130            STANDARD_WEEKMASK,
2131            nyse_rules(),
2132            Some(options_trading_hours()),
2133        )
2134        .with_early_closes(nyse_early_closes()),
2135        UsBondSifma => Calendar::with_type(
2136            name,
2137            MarketType::Bond,
2138            STANDARD_WEEKMASK,
2139            sifma_us_rules(),
2140            Some(sifma_us_hours()),
2141        ),
2142        UsFuturesCme => Calendar::with_type(
2143            name,
2144            MarketType::Futures,
2145            STANDARD_WEEKMASK,
2146            cme_globex_rules(),
2147            Some(cme_globex_overnight_hours()),
2148        ),
2149        UsFuturesCmeEnergy => Calendar::with_type(
2150            name,
2151            MarketType::Futures,
2152            STANDARD_WEEKMASK,
2153            cme_globex_rules(),
2154            Some(cme_globex_energy_hours()),
2155        ),
2156        UsFuturesIce => Calendar::with_type(
2157            name,
2158            MarketType::Futures,
2159            STANDARD_WEEKMASK,
2160            ice_us_rules(),
2161            Some(ice_us_hours()),
2162        ),
2163        UsFuturesCfe => Calendar::with_type(
2164            name,
2165            MarketType::Futures,
2166            STANDARD_WEEKMASK,
2167            cfe_rules(),
2168            Some(cfe_trading_hours()),
2169        ),
2170        Forex24x5 => Calendar::with_type(
2171            name,
2172            MarketType::Fx,
2173            STANDARD_WEEKMASK,
2174            forex_rules(),
2175            Some(TradingHours::forex_24x5()),
2176        ),
2177        Crypto24x7 => Calendar::with_type(
2178            name,
2179            MarketType::Crypto,
2180            CRYPTO_WEEKMASK,
2181            crypto_rules(),
2182            Some(TradingHours::crypto_24x7()),
2183        ),
2184        Lse => Calendar::with_type(
2185            name,
2186            MarketType::Equity,
2187            STANDARD_WEEKMASK,
2188            lse_rules(),
2189            Some(lse_trading_hours()),
2190        ),
2191        Tse => Calendar::with_type(
2192            name,
2193            MarketType::Equity,
2194            STANDARD_WEEKMASK,
2195            tse_rules(),
2196            Some(tse_trading_hours()),
2197        ),
2198        Hkex => Calendar::with_type(
2199            name,
2200            MarketType::Equity,
2201            STANDARD_WEEKMASK,
2202            hkex_rules(),
2203            Some(hkex_trading_hours()),
2204        ),
2205        Sse => Calendar::with_type(
2206            name,
2207            MarketType::Equity,
2208            STANDARD_WEEKMASK,
2209            sse_rules(),
2210            Some(sse_trading_hours()),
2211        ),
2212        Xetra => Calendar::with_type(
2213            name,
2214            MarketType::Equity,
2215            STANDARD_WEEKMASK,
2216            xetra_rules(),
2217            Some(xetra_trading_hours()),
2218        ),
2219        EuronextParis => Calendar::with_type(
2220            name,
2221            MarketType::Equity,
2222            STANDARD_WEEKMASK,
2223            euronext_paris_rules(),
2224            Some(euronext_paris_trading_hours()),
2225        ),
2226        EuronextAms => Calendar::with_type(
2227            name,
2228            MarketType::Equity,
2229            STANDARD_WEEKMASK,
2230            xams_rules(),
2231            Some(euronext_hours(chrono_tz::Europe::Amsterdam)),
2232        ),
2233        EuronextBru => Calendar::with_type(
2234            name,
2235            MarketType::Equity,
2236            STANDARD_WEEKMASK,
2237            xbru_rules(),
2238            Some(euronext_hours(chrono_tz::Europe::Brussels)),
2239        ),
2240        EuronextLis => Calendar::with_type(
2241            name,
2242            MarketType::Equity,
2243            STANDARD_WEEKMASK,
2244            xlis_rules(),
2245            Some(euronext_hours(chrono_tz::Europe::Lisbon)),
2246        ),
2247        EuronextDub => Calendar::with_type(
2248            name,
2249            MarketType::Equity,
2250            STANDARD_WEEKMASK,
2251            xdub_rules(),
2252            Some(xdub_hours()),
2253        ),
2254        Tsx => Calendar::with_type(
2255            name,
2256            MarketType::Equity,
2257            STANDARD_WEEKMASK,
2258            tsx_rules(),
2259            Some(tsx_trading_hours()),
2260        ),
2261        Asx => Calendar::with_type(
2262            name,
2263            MarketType::Equity,
2264            STANDARD_WEEKMASK,
2265            asx_rules(),
2266            Some(asx_trading_hours()),
2267        ),
2268        Nse => Calendar::with_type(
2269            name,
2270            MarketType::Equity,
2271            STANDARD_WEEKMASK,
2272            nse_rules(),
2273            Some(nse_trading_hours()),
2274        ),
2275        Xmil => Calendar::with_type(
2276            name,
2277            MarketType::Equity,
2278            STANDARD_WEEKMASK,
2279            xmil_rules(),
2280            Some(xmil_hours()),
2281        ),
2282        Xmad => Calendar::with_type(
2283            name,
2284            MarketType::Equity,
2285            STANDARD_WEEKMASK,
2286            xmad_rules(),
2287            Some(xmad_hours()),
2288        ),
2289        Xswx => Calendar::with_type(
2290            name,
2291            MarketType::Equity,
2292            STANDARD_WEEKMASK,
2293            xswx_rules(),
2294            Some(xswx_hours()),
2295        ),
2296        Xosl => Calendar::with_type(
2297            name,
2298            MarketType::Equity,
2299            STANDARD_WEEKMASK,
2300            xosl_rules(),
2301            Some(xosl_hours()),
2302        ),
2303        Xsto => Calendar::with_type(
2304            name,
2305            MarketType::Equity,
2306            STANDARD_WEEKMASK,
2307            xsto_rules(),
2308            Some(xsto_hours()),
2309        ),
2310        Xhel => Calendar::with_type(
2311            name,
2312            MarketType::Equity,
2313            STANDARD_WEEKMASK,
2314            xhel_rules(),
2315            Some(xhel_hours()),
2316        ),
2317        Xcse => Calendar::with_type(
2318            name,
2319            MarketType::Equity,
2320            STANDARD_WEEKMASK,
2321            xcse_rules(),
2322            Some(xcse_hours()),
2323        ),
2324        Xice => Calendar::with_type(
2325            name,
2326            MarketType::Equity,
2327            STANDARD_WEEKMASK,
2328            xice_rules(),
2329            Some(xice_hours()),
2330        ),
2331        Xwar => Calendar::with_type(
2332            name,
2333            MarketType::Equity,
2334            STANDARD_WEEKMASK,
2335            xwar_rules(),
2336            Some(xwar_hours()),
2337        ),
2338        Xpra => Calendar::with_type(
2339            name,
2340            MarketType::Equity,
2341            STANDARD_WEEKMASK,
2342            xpra_rules(),
2343            Some(xpra_hours()),
2344        ),
2345        Xbud => Calendar::with_type(
2346            name,
2347            MarketType::Equity,
2348            STANDARD_WEEKMASK,
2349            xbud_rules(),
2350            Some(xbud_hours()),
2351        ),
2352        Xwbo => Calendar::with_type(
2353            name,
2354            MarketType::Equity,
2355            STANDARD_WEEKMASK,
2356            xwbo_rules(),
2357            Some(xwbo_hours()),
2358        ),
2359        Xkrx => Calendar::with_type(
2360            name,
2361            MarketType::Equity,
2362            STANDARD_WEEKMASK,
2363            xkrx_rules(),
2364            Some(xkrx_hours()),
2365        ),
2366        Xses => Calendar::with_type(
2367            name,
2368            MarketType::Equity,
2369            STANDARD_WEEKMASK,
2370            xses_rules(),
2371            Some(xses_hours()),
2372        ),
2373        Xtai => Calendar::with_type(
2374            name,
2375            MarketType::Equity,
2376            STANDARD_WEEKMASK,
2377            xtai_rules(),
2378            Some(xtai_hours()),
2379        ),
2380        Xbkk => Calendar::with_type(
2381            name,
2382            MarketType::Equity,
2383            STANDARD_WEEKMASK,
2384            xbkk_rules(),
2385            Some(xbkk_hours()),
2386        ),
2387        Xkls => Calendar::with_type(
2388            name,
2389            MarketType::Equity,
2390            STANDARD_WEEKMASK,
2391            xkls_rules(),
2392            Some(xkls_hours()),
2393        ),
2394        Xidx => Calendar::with_type(
2395            name,
2396            MarketType::Equity,
2397            STANDARD_WEEKMASK,
2398            xidx_rules(),
2399            Some(xidx_hours()),
2400        ),
2401        Xphs => Calendar::with_type(
2402            name,
2403            MarketType::Equity,
2404            STANDARD_WEEKMASK,
2405            xphs_rules(),
2406            Some(xphs_hours()),
2407        ),
2408        Xnze => Calendar::with_type(
2409            name,
2410            MarketType::Equity,
2411            STANDARD_WEEKMASK,
2412            xnze_rules(),
2413            Some(xnze_hours()),
2414        ),
2415        Xjse => Calendar::with_type(
2416            name,
2417            MarketType::Equity,
2418            STANDARD_WEEKMASK,
2419            xjse_rules(),
2420            Some(xjse_hours()),
2421        ),
2422        Xsau => Calendar::with_type(
2423            name,
2424            MarketType::Equity,
2425            MIDEAST_WEEKMASK,
2426            xsau_rules(),
2427            Some(xsau_hours()),
2428        ),
2429        Xist => Calendar::with_type(
2430            name,
2431            MarketType::Equity,
2432            STANDARD_WEEKMASK,
2433            xist_rules(),
2434            Some(xist_hours()),
2435        ),
2436        Xtae => Calendar::with_type(
2437            name,
2438            MarketType::Equity,
2439            TASE_WEEKMASK,
2440            xtae_rules(),
2441            Some(xtae_hours()),
2442        ),
2443        Xdfm => Calendar::with_type(
2444            name,
2445            MarketType::Equity,
2446            STANDARD_WEEKMASK,
2447            xdfm_rules(),
2448            Some(xdfm_hours()),
2449        ),
2450        Bvmf => Calendar::with_type(
2451            name,
2452            MarketType::Equity,
2453            STANDARD_WEEKMASK,
2454            bvmf_rules(),
2455            Some(bvmf_hours()),
2456        ),
2457        Xmex => Calendar::with_type(
2458            name,
2459            MarketType::Equity,
2460            STANDARD_WEEKMASK,
2461            xmex_rules(),
2462            Some(xmex_hours()),
2463        ),
2464        Xbue => Calendar::with_type(
2465            name,
2466            MarketType::Equity,
2467            STANDARD_WEEKMASK,
2468            xbue_rules(),
2469            Some(xbue_hours()),
2470        ),
2471        Xsgo => Calendar::with_type(
2472            name,
2473            MarketType::Equity,
2474            STANDARD_WEEKMASK,
2475            xsgo_rules(),
2476            Some(xsgo_hours()),
2477        ),
2478        Xlim => Calendar::with_type(
2479            name,
2480            MarketType::Equity,
2481            STANDARD_WEEKMASK,
2482            xlim_rules(),
2483            Some(xlim_hours()),
2484        ),
2485        Xbog => Calendar::with_type(
2486            name,
2487            MarketType::Equity,
2488            STANDARD_WEEKMASK,
2489            xbog_rules(),
2490            Some(xbog_hours()),
2491        ),
2492    }
2493}
2494
2495/// Build a calendar from its MIC code (or a generic family name like
2496/// `FOREX`, `CRYPTO`, `SIFMA_US`, `ICE_US`, `CFE`). Returns `None` if unknown.
2497pub fn calendar_for_exchange(code: &str) -> Option<Calendar> {
2498    let upper = code.to_ascii_uppercase();
2499    let fam = family_for_mic(&upper)?;
2500    Some(build_family(&upper, fam))
2501}
2502
2503/// Build a calendar from a region code. Returns `None` if unknown.
2504pub fn calendar_for_region(code: &str) -> Option<Calendar> {
2505    match code.to_ascii_uppercase().as_str() {
2506        "US" => calendar_for_exchange("XNYS"),
2507        "UK" | "GB" => calendar_for_exchange("XLON"),
2508        "JP" => calendar_for_exchange("XTKS"),
2509        "HK" => calendar_for_exchange("XHKG"),
2510        "CN" => calendar_for_exchange("XSHG"),
2511        "DE" | "EU" => calendar_for_exchange("XFRA"),
2512        "FR" => calendar_for_exchange("XPAR"),
2513        "CA" => calendar_for_exchange("XTSE"),
2514        "AU" => calendar_for_exchange("XASX"),
2515        "IN" => calendar_for_exchange("XNSE"),
2516        "NL" => calendar_for_exchange("XAMS"),
2517        "BE" => calendar_for_exchange("XBRU"),
2518        "PT" => calendar_for_exchange("XLIS"),
2519        "IT" => calendar_for_exchange("XMIL"),
2520        "ES" => calendar_for_exchange("XMAD"),
2521        "CH" => calendar_for_exchange("XSWX"),
2522        "NO" => calendar_for_exchange("XOSL"),
2523        "SE" => calendar_for_exchange("XSTO"),
2524        "FI" => calendar_for_exchange("XHEL"),
2525        "DK" => calendar_for_exchange("XCSE"),
2526        "IS" => calendar_for_exchange("XICE"),
2527        "PL" => calendar_for_exchange("XWAR"),
2528        "CZ" => calendar_for_exchange("XPRA"),
2529        "HU" => calendar_for_exchange("XBUD"),
2530        "AT" => calendar_for_exchange("XWBO"),
2531        "IE" => calendar_for_exchange("XDUB"),
2532        "KR" => calendar_for_exchange("XKRX"),
2533        "SG" => calendar_for_exchange("XSES"),
2534        "TW" => calendar_for_exchange("XTAI"),
2535        "TH" => calendar_for_exchange("XBKK"),
2536        "MY" => calendar_for_exchange("XKLS"),
2537        "ID" => calendar_for_exchange("XIDX"),
2538        "PH" => calendar_for_exchange("XPHS"),
2539        "NZ" => calendar_for_exchange("XNZE"),
2540        "ZA" => calendar_for_exchange("XJSE"),
2541        "SA" => calendar_for_exchange("XSAU"),
2542        "TR" => calendar_for_exchange("XIST"),
2543        "IL" => calendar_for_exchange("XTAE"),
2544        "AE" => calendar_for_exchange("XDFM"),
2545        "BR" => calendar_for_exchange("BVMF"),
2546        "MX" => calendar_for_exchange("XMEX"),
2547        "AR" => calendar_for_exchange("XBUE"),
2548        "CL" => calendar_for_exchange("XSGO"),
2549        "PE" => calendar_for_exchange("XLIM"),
2550        "CO" => calendar_for_exchange("XBOG"),
2551        _ => None,
2552    }
2553}
2554
2555#[cfg(test)]
2556mod tests {
2557    use super::*;
2558    use chrono::TimeZone;
2559    use chrono::Timelike;
2560
2561    #[test]
2562    fn nyse_2024_business_day_count() {
2563        let cal = calendar_for_exchange("XNYS").unwrap();
2564        let n = cal.business_days_between(
2565            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
2566            NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
2567        );
2568        assert_eq!(n, 252);
2569    }
2570
2571    #[test]
2572    fn nyse_christmas_2022_observed_monday() {
2573        let cal = calendar_for_exchange("XNYS").unwrap();
2574        assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2022, 12, 26).unwrap()));
2575    }
2576
2577    #[test]
2578    fn nyse_juneteenth_first_year_2021() {
2579        let cal = calendar_for_exchange("XNYS").unwrap();
2580        assert!(!cal.is_holiday(NaiveDate::from_ymd_opt(2020, 6, 19).unwrap()));
2581        assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2021, 6, 18).unwrap()));
2582    }
2583
2584    #[test]
2585    fn lse_easter_monday_2024() {
2586        let cal = calendar_for_exchange("XLON").unwrap();
2587        assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2024, 4, 1).unwrap()));
2588    }
2589
2590    #[test]
2591    fn region_us_resolves_to_xnys() {
2592        let cal = calendar_for_region("US").unwrap();
2593        assert_eq!(cal.name, "XNYS");
2594    }
2595
2596    #[test]
2597    fn nyse_is_open_at_market_open() {
2598        let cal = calendar_for_exchange("XNYS").unwrap();
2599        let inst = chrono_tz::America::New_York
2600            .with_ymd_and_hms(2024, 1, 8, 9, 30, 0)
2601            .unwrap()
2602            .with_timezone(&Utc);
2603        assert!(cal.is_open(inst));
2604        let inst_b = chrono_tz::America::New_York
2605            .with_ymd_and_hms(2024, 1, 8, 9, 27, 0)
2606            .unwrap()
2607            .with_timezone(&Utc);
2608        assert!(!cal.is_open(inst_b));
2609    }
2610
2611    #[test]
2612    fn nyse_is_open_handles_dst() {
2613        let cal = calendar_for_exchange("XNYS").unwrap();
2614        let inst = chrono_tz::America::New_York
2615            .with_ymd_and_hms(2024, 3, 11, 9, 30, 0)
2616            .unwrap()
2617            .with_timezone(&Utc);
2618        assert!(cal.is_open(inst));
2619    }
2620
2621    #[test]
2622    fn cme_futures_open_sunday_evening() {
2623        // CME equity-index futures: Sun 18:00 CT should be in Mon's session.
2624        let cal = calendar_for_exchange("XCME").unwrap();
2625        assert_eq!(cal.market_type, MarketType::Futures);
2626        let inst = chrono_tz::America::Chicago
2627            .with_ymd_and_hms(2024, 1, 7, 18, 0, 0)
2628            .unwrap()
2629            .with_timezone(&Utc);
2630        assert!(cal.is_open(inst));
2631        // Sat 03:00 CT — closed.
2632        let inst2 = chrono_tz::America::Chicago
2633            .with_ymd_and_hms(2024, 1, 13, 3, 0, 0)
2634            .unwrap()
2635            .with_timezone(&Utc);
2636        assert!(!cal.is_open(inst2));
2637    }
2638
2639    #[test]
2640    fn nymex_energy_uses_chicago_tz() {
2641        let cal = calendar_for_exchange("XNYM").unwrap();
2642        assert_eq!(cal.market_type, MarketType::Futures);
2643        // Mon 09:00 CT → in session (started Sun 17:00 CT).
2644        let inst = chrono_tz::America::Chicago
2645            .with_ymd_and_hms(2024, 1, 8, 9, 0, 0)
2646            .unwrap()
2647            .with_timezone(&Utc);
2648        assert!(cal.is_open(inst));
2649    }
2650
2651    #[test]
2652    fn cfe_classifies_as_futures() {
2653        let cal = calendar_for_exchange("CFE").unwrap();
2654        assert_eq!(cal.market_type, MarketType::Futures);
2655        // Wed 09:00 CT — open.
2656        let inst = chrono_tz::America::Chicago
2657            .with_ymd_and_hms(2024, 1, 10, 9, 0, 0)
2658            .unwrap()
2659            .with_timezone(&Utc);
2660        assert!(cal.is_open(inst));
2661    }
2662
2663    #[test]
2664    fn forex_open_tuesday_3am() {
2665        let cal = calendar_for_exchange("FOREX").unwrap();
2666        assert_eq!(cal.market_type, MarketType::Fx);
2667        let inst = chrono_tz::America::New_York
2668            .with_ymd_and_hms(2024, 1, 9, 3, 0, 0)
2669            .unwrap()
2670            .with_timezone(&Utc);
2671        assert!(cal.is_open(inst));
2672    }
2673
2674    #[test]
2675    fn crypto_open_saturday_3am() {
2676        let cal = calendar_for_exchange("CRYPTO").unwrap();
2677        assert_eq!(cal.market_type, MarketType::Crypto);
2678        let inst = chrono_tz::UTC
2679            .with_ymd_and_hms(2024, 1, 13, 3, 0, 0)
2680            .unwrap()
2681            .with_timezone(&Utc);
2682        assert!(cal.is_open(inst));
2683    }
2684
2685    #[test]
2686    fn options_close_at_1615() {
2687        let cal = calendar_for_exchange("OPRA").unwrap();
2688        assert_eq!(cal.market_type, MarketType::Options);
2689        let inst = chrono_tz::America::New_York
2690            .with_ymd_and_hms(2024, 1, 8, 16, 10, 0)
2691            .unwrap()
2692            .with_timezone(&Utc);
2693        assert!(cal.is_open(inst));
2694    }
2695
2696    #[test]
2697    fn sifma_includes_columbus_and_veterans() {
2698        let cal = calendar_for_exchange("SIFMA_US").unwrap();
2699        assert_eq!(cal.market_type, MarketType::Bond);
2700        // Veterans Day 2024 = Mon Nov 11.
2701        assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2024, 11, 11).unwrap()));
2702        // Columbus Day 2024 = 2nd Mon Oct = Oct 14.
2703        assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2024, 10, 14).unwrap()));
2704    }
2705
2706    #[test]
2707    fn ice_us_uses_overnight_session() {
2708        let cal = calendar_for_exchange("ICE_US").unwrap();
2709        // Sun 21:00 NY should be in Mon's ICE session.
2710        let inst = chrono_tz::America::New_York
2711            .with_ymd_and_hms(2024, 1, 7, 21, 0, 0)
2712            .unwrap()
2713            .with_timezone(&Utc);
2714        assert!(cal.is_open(inst));
2715    }
2716
2717    #[test]
2718    fn all_exchange_codes_resolve() {
2719        for code in EXCHANGE_CODES {
2720            assert!(
2721                calendar_for_exchange(code).is_some(),
2722                "MIC {} did not resolve",
2723                code
2724            );
2725        }
2726    }
2727
2728    #[test]
2729    fn exchange_codes_are_sourced_from_finance_enums() {
2730        assert_eq!(EXCHANGE_CODES, finance_enums::data::ExchangeCode_VARIANTS);
2731        assert!(std::ptr::eq(
2732            EXCHANGE_CODES.as_ptr(),
2733            finance_enums::data::ExchangeCode_VARIANTS.as_ptr()
2734        ));
2735    }
2736
2737    #[test]
2738    fn otc_inherits_nyse_holidays() {
2739        let cal = calendar_for_exchange("PINX").unwrap();
2740        assert_eq!(cal.market_type, MarketType::Equity);
2741        assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2024, 7, 4).unwrap()));
2742    }
2743
2744    #[test]
2745    fn canadian_calendar_for_neoe() {
2746        let cal = calendar_for_exchange("NEOE").unwrap();
2747        // Canada Day 2024 = Mon Jul 1.
2748        assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2024, 7, 1).unwrap()));
2749    }
2750
2751    #[test]
2752    fn nyse_july3_2024_is_early_close() {
2753        // July 4 2024 was Thursday; July 3 (Wed) had a 13:00 ET early close.
2754        let cal = calendar_for_exchange("XNYS").unwrap();
2755        let day = NaiveDate::from_ymd_opt(2024, 7, 3).unwrap();
2756        assert_eq!(
2757            cal.early_close_for(day),
2758            Some(NaiveTime::from_hms_opt(13, 0, 0).unwrap())
2759        );
2760        // 14:00 ET that day should be CLOSED.
2761        let inst = chrono_tz::America::New_York
2762            .with_ymd_and_hms(2024, 7, 3, 14, 0, 0)
2763            .unwrap()
2764            .with_timezone(&Utc);
2765        assert!(!cal.is_open(inst));
2766        // 12:30 ET should be OPEN.
2767        let inst2 = chrono_tz::America::New_York
2768            .with_ymd_and_hms(2024, 7, 3, 12, 30, 0)
2769            .unwrap()
2770            .with_timezone(&Utc);
2771        assert!(cal.is_open(inst2));
2772    }
2773
2774    #[test]
2775    fn nyse_black_friday_2024_early_close() {
2776        // 2024 Thanksgiving = Thu Nov 28; Black Friday = Nov 29.
2777        let cal = calendar_for_exchange("XNYS").unwrap();
2778        assert_eq!(
2779            cal.early_close_for(NaiveDate::from_ymd_opt(2024, 11, 29).unwrap()),
2780            Some(NaiveTime::from_hms_opt(13, 0, 0).unwrap())
2781        );
2782    }
2783
2784    #[test]
2785    fn xams_kingsday_2024() {
2786        // Apr 27 2024 falls on a Saturday; King's Day skipped (no roll).
2787        // Test 2023 instead: Apr 27 2023 = Thursday, holiday.
2788        let cal = calendar_for_exchange("XAMS").unwrap();
2789        assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2023, 4, 27).unwrap()));
2790    }
2791
2792    #[test]
2793    fn xkrx_seollal_2024_multi_day() {
2794        // Korean Lunar NY 2024 spans Feb 9 (Fri) and Feb 12 (Mon).
2795        let cal = calendar_for_exchange("XKRX").unwrap();
2796        assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2024, 2, 9).unwrap()));
2797        assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2024, 2, 12).unwrap()));
2798    }
2799
2800    #[test]
2801    fn xtae_uses_sun_thu_weekmask() {
2802        let cal = calendar_for_exchange("XTAE").unwrap();
2803        // Sun May 5 2024 should be a business day at TASE.
2804        assert!(cal.is_business_day(NaiveDate::from_ymd_opt(2024, 5, 5).unwrap()));
2805        // Fri May 3 2024 — weekend.
2806        assert!(!cal.is_business_day(NaiveDate::from_ymd_opt(2024, 5, 3).unwrap()));
2807    }
2808
2809    #[test]
2810    fn xsau_uses_sun_thu_weekmask() {
2811        let cal = calendar_for_exchange("XSAU").unwrap();
2812        assert!(cal.is_business_day(NaiveDate::from_ymd_opt(2024, 5, 5).unwrap()));
2813        assert!(!cal.is_business_day(NaiveDate::from_ymd_opt(2024, 5, 3).unwrap()));
2814    }
2815
2816    #[test]
2817    fn bvmf_carnival_2024() {
2818        // 2024: Easter Apr 1 → Carnival Mon Feb 12, Tue Feb 13.
2819        let cal = calendar_for_exchange("BVMF").unwrap();
2820        assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2024, 2, 12).unwrap()));
2821        assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2024, 2, 13).unwrap()));
2822    }
2823
2824    #[test]
2825    fn region_br_resolves_to_bvmf() {
2826        let cal = calendar_for_region("BR").unwrap();
2827        assert_eq!(cal.name, "BVMF");
2828    }
2829
2830    #[test]
2831    fn xnze_waitangi_2024() {
2832        let cal = calendar_for_exchange("XNZE").unwrap();
2833        // Feb 6 2024 = Tuesday.
2834        assert!(cal.is_holiday(NaiveDate::from_ymd_opt(2024, 2, 6).unwrap()));
2835    }
2836
2837    #[test]
2838    fn nyse_sessions_between_one_week_with_early_close() {
2839        let cal = calendar_for_exchange("XNYS").unwrap();
2840        // Mon Jul 1 — Fri Jul 5 2024. Jul 4 = holiday; Jul 3 = early close.
2841        let s = cal.sessions_between(
2842            NaiveDate::from_ymd_opt(2024, 7, 1).unwrap(),
2843            NaiveDate::from_ymd_opt(2024, 7, 5).unwrap(),
2844        );
2845        assert_eq!(s.len(), 4);
2846        // Jul 3 close (3rd entry) should be 13:00 ET = 17:00 UTC (EDT = UTC-4).
2847        let jul3_close_local = s[2].1.with_timezone(&chrono_tz::America::New_York);
2848        assert_eq!(jul3_close_local.hour(), 13);
2849        assert_eq!(jul3_close_local.minute(), 0);
2850        // Jul 5 close (4th entry) should be 16:00 ET (regular).
2851        let jul5_close_local = s[3].1.with_timezone(&chrono_tz::America::New_York);
2852        assert_eq!(jul5_close_local.hour(), 16);
2853    }
2854
2855    #[test]
2856    fn nyse_holidays_between_q3_2024() {
2857        let cal = calendar_for_exchange("XNYS").unwrap();
2858        let h = cal.holidays_between(
2859            NaiveDate::from_ymd_opt(2024, 7, 1).unwrap(),
2860            NaiveDate::from_ymd_opt(2024, 9, 30).unwrap(),
2861        );
2862        // Jul 4 (Independence Day) and Sep 2 (Labor Day).
2863        assert!(h.contains(&NaiveDate::from_ymd_opt(2024, 7, 4).unwrap()));
2864        assert!(h.contains(&NaiveDate::from_ymd_opt(2024, 9, 2).unwrap()));
2865        assert_eq!(h.len(), 2);
2866    }
2867}