usec/
calendar.rs

1//! Implementation of US stock exchange calendar with full and half-day holidays.
2//! code borrowed heavily from
3//! <https://github.com/xemwebe/cal-calc>
4
5use chrono::{Datelike, Duration, NaiveDate, Weekday};
6use serde::{Deserialize, Serialize};
7use std::collections::BTreeSet;
8use std::env;
9
10/// Specifies the nth week of a month
11#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
12pub enum NthWeek {
13    First,
14    Second,
15    Third,
16    Fourth,
17    Last,
18}
19/// Do the half-day holiday check before or after the target date
20#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
21pub enum HalfCheck {
22    Before,
23    After,
24}
25
26/// Types of days when US stocks exchanges are closed
27#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
28pub enum Holiday {
29    /// for US exchanges, `Sat` and `Sun`
30    WeekDay(Weekday),
31    /// `first` and `last` are the first and last year this day is a holiday (inclusively).
32    MovableYearlyDay {
33        month: u32,
34        day: u32,
35        first: Option<i32>,
36        last: Option<i32>,
37        half_check: Option<HalfCheck>,
38    },
39    /// A single holiday which is valid only once in time.
40    SingularDay(NaiveDate),
41    /// A holiday that is defined in relative days (e.g. -2 for Good Friday) to Easter (Sunday).
42    EasterOffset {
43        offset: i32,
44        first: Option<i32>,
45        last: Option<i32>,
46    },
47    /// A holiday that falls on the nth (or last) weekday of a specific month, e.g. the first Monday in May.
48    /// `first` and `last` are the first and last year this day is a holiday (inclusively).
49    MonthWeekday {
50        month: u32,
51        weekday: Weekday,
52        nth: NthWeek,
53        first: Option<i32>,
54        last: Option<i32>,
55        half_check: Option<HalfCheck>,
56    },
57}
58
59/// Calendar for arbitrary complex holiday rules
60#[derive(Debug, Clone)]
61pub struct Calendar {
62    holidays: BTreeSet<NaiveDate>,
63    halfdays: BTreeSet<NaiveDate>,
64    weekdays: Vec<Weekday>,
65}
66
67impl Calendar {
68    /// Calculate all holidays and recognize weekend days for a given range of years
69    /// from `start` to `end` (inclusively). The calculation is performed on the basis
70    /// of a vector of holiday rules.
71    pub fn calc_calendar(holiday_rules: &[Holiday], start: i32, end: i32) -> Calendar {
72        let mut holidays = BTreeSet::new();
73        let mut halfdays = BTreeSet::new();
74        let mut weekdays = Vec::new();
75
76        for rule in holiday_rules {
77            match rule {
78                Holiday::SingularDay(date) => {
79                    let year = date.year();
80                    if year >= start && year <= end {
81                        holidays.insert(*date);
82                    }
83                }
84                Holiday::WeekDay(weekday) => {
85                    weekdays.push(*weekday);
86                }
87                // check if prior to 7/4 and 12/25
88                Holiday::MovableYearlyDay {
89                    month,
90                    day,
91                    first,
92                    last,
93                    half_check,
94                } => {
95                    let (first, last) = Self::calc_first_and_last(start, end, first, last);
96                    for year in first..last + 1 {
97                        let date = Calendar::from_ymd(year, *month, *day);
98                        // if date falls on Saturday, use Friday, if date falls on Sunday, use Monday
99                        let orig_wd = date.weekday();
100                        let mut moved_already = false;
101                        let date = match orig_wd {
102                            Weekday::Sat => {
103                                moved_already = true;
104                                date.pred_opt().unwrap()
105                            }
106                            Weekday::Sun => {
107                                moved_already = true;
108                                date.succ_opt().unwrap()
109                            }
110                            _ => date,
111                        };
112                        let (last_date_of_month, last_date_of_year) = accounting_period_end(date);
113                        // use the date only if it's not the end of a month or a year
114                        if date != last_date_of_month && date != last_date_of_year {
115                            holidays.insert(date);
116                            if !moved_already {
117                                do_halfday_check(&date, &mut halfdays, half_check);
118                            }
119                        }
120                    }
121                }
122                Holiday::EasterOffset {
123                    offset,
124                    first,
125                    last,
126                } => {
127                    let (first, last) = Self::calc_first_and_last(start, end, first, last);
128                    for year in first..last + 1 {
129                        let easter = computus::gregorian(year).unwrap();
130                        let easter = Calendar::from_ymd(easter.year, easter.month, easter.day);
131                        let date = easter
132                            .checked_add_signed(Duration::days(*offset as i64))
133                            .unwrap();
134                        holidays.insert(date);
135                    }
136                }
137                Holiday::MonthWeekday {
138                    month,
139                    weekday,
140                    nth,
141                    first,
142                    last,
143                    half_check,
144                } => {
145                    let (first, last) = Self::calc_first_and_last(start, end, first, last);
146                    for year in first..last + 1 {
147                        let day = match nth {
148                            NthWeek::First => 1,
149                            NthWeek::Second => 8,
150                            NthWeek::Third => 15,
151                            NthWeek::Fourth => 22,
152                            NthWeek::Last => last_day_of_month(year, *month),
153                        };
154                        let mut date = Calendar::from_ymd(year, *month, day);
155                        while date.weekday() != *weekday {
156                            date = match nth {
157                                NthWeek::Last => date.pred_opt().unwrap(),
158                                _ => date.succ_opt().unwrap(),
159                            }
160                        }
161                        holidays.insert(date);
162                        do_halfday_check(&date, &mut halfdays, half_check);
163                    }
164                }
165            }
166        }
167        Calendar {
168            holidays,
169            halfdays,
170            weekdays,
171        }
172    }
173
174    /// Calculate the next business day
175    pub fn next_biz_day(&self, date: NaiveDate) -> NaiveDate {
176        let mut date = date.succ_opt().unwrap();
177        while !self.is_business_day(date) {
178            date = date.succ_opt().unwrap();
179        }
180        date
181    }
182
183    /// Calculate the previous business day
184    pub fn prev_biz_day(&self, date: NaiveDate) -> NaiveDate {
185        let mut date = date.pred_opt().unwrap();
186        while !self.is_business_day(date) {
187            date = date.pred_opt().unwrap();
188        }
189        date
190    }
191
192    fn calc_first_and_last(
193        start: i32,
194        end: i32,
195        first: &Option<i32>,
196        last: &Option<i32>,
197    ) -> (i32, i32) {
198        let first = match first {
199            Some(year) => std::cmp::max(start, *year),
200            _ => start,
201        };
202        let last = match last {
203            Some(year) => std::cmp::min(end, *year),
204            _ => end,
205        };
206        (first, last)
207    }
208
209    /// Returns true if the date falls on a weekend
210    pub fn is_weekend(&self, day: NaiveDate) -> bool {
211        let weekday = day.weekday();
212        for w_day in &self.weekdays {
213            if weekday == *w_day {
214                return true;
215            }
216        }
217        false
218    }
219
220    /// Returns true if the specified day is a full-day holiday
221    pub fn is_holiday(&self, date: NaiveDate) -> bool {
222        self.holidays.contains(&date)
223    }
224
225    /// Returns true if the specified day is a half-day holiday
226    pub fn is_half_holiday(&self, date: NaiveDate) -> bool {
227        self.halfdays.contains(&date)
228    }
229
230    /// Returns true if the specified day is a business day
231    pub fn is_business_day(&self, date: NaiveDate) -> bool {
232        !self.is_weekend(date) && !self.is_holiday(date)
233    }
234
235    pub fn from_ymd(year: i32, month: u32, day: u32) -> NaiveDate {
236        NaiveDate::from_ymd_opt(year, month, day).unwrap()
237    }
238}
239
240/// Returns true if the specified year is a leap year (i.e. Feb 29th exists for this year)
241pub fn is_leap_year(year: i32) -> bool {
242    NaiveDate::from_ymd_opt(year, 2, 29).is_some()
243}
244
245/// Returns ending accounting period (end of month, end of year)
246pub fn accounting_period_end(date: NaiveDate) -> (NaiveDate, NaiveDate) {
247    let month = date.month();
248    let year = date.year();
249    let last_date_of_month = NaiveDate::from_ymd_opt(year, month + 1, 1)
250        .unwrap_or_else(|| Calendar::from_ymd(year + 1, 1, 1))
251        .pred_opt()
252        .unwrap();
253    let last_date_of_year = NaiveDate::from_ymd_opt(year, 12, 31).unwrap();
254    (last_date_of_month, last_date_of_year)
255}
256
257pub fn do_halfday_check(
258    date: &NaiveDate,
259    halfdays: &mut BTreeSet<NaiveDate>,
260    half_check: &Option<HalfCheck>,
261) {
262    let weekday = date.weekday();
263    match half_check {
264        None => {}
265        Some(HalfCheck::Before) => {
266            if weekday == Weekday::Mon {
267                return;
268            }
269            let prior = date.pred_opt().unwrap();
270            halfdays.insert(prior);
271        }
272        Some(HalfCheck::After) => {
273            if weekday == Weekday::Fri {
274                return;
275            }
276            let next = date.succ_opt().unwrap();
277            halfdays.insert(next);
278        }
279    }
280}
281
282/// Calculate the last day of a given month in a given year
283pub fn last_day_of_month(year: i32, month: u32) -> u32 {
284    NaiveDate::from_ymd_opt(year, month + 1, 1)
285        .unwrap_or_else(|| Calendar::from_ymd(year + 1, 1, 1))
286        .pred_opt()
287        .unwrap()
288        .day()
289}
290
291/// Calendar specific to US stock exchanges
292#[derive(Debug, Clone)]
293pub struct UsExchangeCalendar {
294    cal: Calendar,
295    holiday_rules: Vec<Holiday>,
296}
297
298impl UsExchangeCalendar {
299    /// NYSE holiday calendar as of 2022
300    /// create a new US Exchange calendar with default rules, populate the
301    /// calendar with default range (2000-2050) if `populate` is set to `true`
302    pub fn with_default_range(populate: bool) -> UsExchangeCalendar {
303        let mut holiday_rules = vec![
304            // Saturdays
305            Holiday::WeekDay(Weekday::Sat),
306            // Sundays
307            Holiday::WeekDay(Weekday::Sun),
308            // New Year's day
309            Holiday::MovableYearlyDay {
310                month: 1,
311                day: 1,
312                first: None,
313                last: None,
314                half_check: None,
315            },
316            // MLK, 3rd Monday of January
317            Holiday::MonthWeekday {
318                month: 1,
319                weekday: Weekday::Mon,
320                nth: NthWeek::Third,
321                first: None,
322                last: None,
323                half_check: None,
324            },
325            // President's Day
326            Holiday::MonthWeekday {
327                month: 2,
328                weekday: Weekday::Mon,
329                nth: NthWeek::Third,
330                first: None,
331                last: None,
332                half_check: None,
333            },
334            // Good Friday
335            Holiday::EasterOffset {
336                offset: -2,
337                first: Some(2000),
338                last: None,
339            },
340            // Memorial Day
341            Holiday::MonthWeekday {
342                month: 5,
343                weekday: Weekday::Mon,
344                nth: NthWeek::Last,
345                first: None,
346                last: None,
347                half_check: None,
348            },
349            // Juneteenth National Independence Day
350            Holiday::MovableYearlyDay {
351                month: 6,
352                day: 19,
353                first: Some(2022),
354                last: None,
355                half_check: None,
356            },
357            // Independence Day
358            Holiday::MovableYearlyDay {
359                month: 7,
360                day: 4,
361                first: None,
362                last: None,
363                half_check: Some(HalfCheck::Before),
364            },
365            // Labour Day
366            Holiday::MonthWeekday {
367                month: 9,
368                weekday: Weekday::Mon,
369                nth: NthWeek::First,
370                first: None,
371                last: None,
372                half_check: None,
373            },
374            // Thanksgiving Day
375            Holiday::MonthWeekday {
376                month: 11,
377                weekday: Weekday::Thu,
378                nth: NthWeek::Fourth,
379                first: None,
380                last: None,
381                half_check: Some(HalfCheck::After),
382            },
383            // Chrismas Day
384            Holiday::MovableYearlyDay {
385                month: 12,
386                day: 25,
387                first: None,
388                last: None,
389                half_check: Some(HalfCheck::Before),
390            },
391            Holiday::SingularDay(Calendar::from_ymd(2001, 9, 11)),
392        ];
393        let additional_rules = env::var("ADDITIONAL_RULES");
394        if additional_rules.is_ok() {
395            let mut additional_rules: Vec<Holiday> =
396                serde_json::from_str(&additional_rules.unwrap()).unwrap();
397            holiday_rules.append(&mut additional_rules);
398        }
399        let cal = Calendar {
400            holidays: BTreeSet::new(),
401            halfdays: BTreeSet::new(),
402            weekdays: Vec::new(),
403        };
404        let mut sc = UsExchangeCalendar { cal, holiday_rules };
405        if populate {
406            sc.populate_cal(None, None);
407        }
408        sc
409    }
410
411    /// add an ad-hoc holiday rule to the rule list
412    pub fn add_holiday_rule(&mut self, holiday: Holiday) -> &mut Self {
413        self.holiday_rules.push(holiday);
414        self
415    }
416
417    /// populate calendar for given `start` and `end` years (inclusively, defaults to 2000 and 2050 if None,
418    /// None are given)
419    pub fn populate_cal(&mut self, start: Option<i32>, end: Option<i32>) -> &mut Self {
420        let start = start.unwrap_or(2000);
421        let end = end.unwrap_or(2050);
422        self.cal = Calendar::calc_calendar(&self.holiday_rules, start, end);
423        self
424    }
425
426    pub fn get_cal(&self) -> Calendar {
427        self.cal.clone()
428    }
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434
435    fn make_cal() -> Calendar {
436        let usec = UsExchangeCalendar::with_default_range(true);
437        usec.get_cal()
438    }
439
440    #[test]
441    fn fixed_dates_calendar() {
442        let holidays = vec![
443            Holiday::SingularDay(Calendar::from_ymd(2019, 11, 20)),
444            Holiday::SingularDay(Calendar::from_ymd(2019, 11, 24)),
445            Holiday::SingularDay(Calendar::from_ymd(2019, 11, 25)),
446            Holiday::WeekDay(Weekday::Sat),
447            Holiday::WeekDay(Weekday::Sun),
448        ];
449        let cal = Calendar::calc_calendar(&holidays, 2019, 2019);
450
451        assert_eq!(false, cal.is_business_day(Calendar::from_ymd(2019, 11, 20)));
452        assert_eq!(true, cal.is_business_day(Calendar::from_ymd(2019, 11, 21)));
453        assert_eq!(true, cal.is_business_day(Calendar::from_ymd(2019, 11, 22)));
454        // weekend
455        assert_eq!(false, cal.is_business_day(Calendar::from_ymd(2019, 11, 23)));
456        assert_eq!(true, cal.is_weekend(Calendar::from_ymd(2019, 11, 23)));
457        assert_eq!(false, cal.is_holiday(Calendar::from_ymd(2019, 11, 23)));
458        // weekend and holiday
459        assert_eq!(false, cal.is_business_day(Calendar::from_ymd(2019, 11, 24)));
460        assert_eq!(true, cal.is_weekend(Calendar::from_ymd(2019, 11, 24)));
461        assert_eq!(true, cal.is_holiday(Calendar::from_ymd(2019, 11, 24)));
462        assert_eq!(false, cal.is_business_day(Calendar::from_ymd(2019, 11, 25)));
463        assert_eq!(true, cal.is_business_day(Calendar::from_ymd(2019, 11, 26)));
464    }
465
466    #[test]
467    fn test_movable_yearly_day() {
468        let holidays = vec![Holiday::MovableYearlyDay {
469            month: 1,
470            day: 1,
471            first: None,
472            last: None,
473            half_check: None,
474        }];
475        let cal = Calendar::calc_calendar(&holidays, 2021, 2022);
476        assert_eq!(false, cal.is_holiday(Calendar::from_ymd(2021, 12, 31)));
477    }
478
479    #[test]
480    /// Good Friday example
481    fn test_easter_offset() {
482        let holidays = vec![Holiday::EasterOffset {
483            offset: -2,
484            first: None,
485            last: None,
486        }];
487        let cal = Calendar::calc_calendar(&holidays, 2021, 2022);
488        assert_eq!(false, cal.is_business_day(Calendar::from_ymd(2021, 4, 2)));
489        assert_eq!(false, cal.is_business_day(Calendar::from_ymd(2022, 4, 15)));
490    }
491
492    #[test]
493    fn test_month_weekday() {
494        let holidays = vec![
495            // MLK
496            Holiday::MonthWeekday {
497                month: 1,
498                weekday: Weekday::Mon,
499                nth: NthWeek::Third,
500                first: None,
501                last: None,
502                half_check: None,
503            },
504            // President's Day
505            Holiday::MonthWeekday {
506                month: 2,
507                weekday: Weekday::Mon,
508                nth: NthWeek::Third,
509                first: None,
510                last: None,
511                half_check: None,
512            },
513        ];
514        let cal = Calendar::calc_calendar(&holidays, 2022, 2022);
515        assert_eq!(true, cal.is_holiday(Calendar::from_ymd(2022, 1, 17)));
516        assert_eq!(true, cal.is_holiday(Calendar::from_ymd(2022, 2, 21)));
517    }
518
519    #[test]
520    /// Testing serialization and deserialization of holidays definitions
521    fn serialize_cal_definition() {
522        let holidays = vec![
523            Holiday::MonthWeekday {
524                month: 11,
525                weekday: Weekday::Mon,
526                nth: NthWeek::First,
527                first: None,
528                last: None,
529                half_check: None,
530            },
531            Holiday::MovableYearlyDay {
532                month: 11,
533                day: 1,
534                first: Some(2016),
535                last: None,
536                half_check: None,
537            },
538            Holiday::SingularDay(Calendar::from_ymd(2019, 11, 25)),
539            Holiday::WeekDay(Weekday::Sat),
540            Holiday::EasterOffset {
541                offset: -2,
542                first: None,
543                last: None,
544            },
545        ];
546        let json = serde_json::to_string_pretty(&holidays).unwrap();
547        assert_eq!(
548            json,
549            r#"[
550  {
551    "MonthWeekday": {
552      "month": 11,
553      "weekday": "Mon",
554      "nth": "First",
555      "first": null,
556      "last": null,
557      "half_check": null
558    }
559  },
560  {
561    "MovableYearlyDay": {
562      "month": 11,
563      "day": 1,
564      "first": 2016,
565      "last": null,
566      "half_check": null
567    }
568  },
569  {
570    "SingularDay": "2019-11-25"
571  },
572  {
573    "WeekDay": "Sat"
574  },
575  {
576    "EasterOffset": {
577      "offset": -2,
578      "first": null,
579      "last": null
580    }
581  }
582]"#
583        );
584        let holidays2: Vec<Holiday> = serde_json::from_str(&json).unwrap();
585        assert_eq!(holidays.len(), holidays2.len());
586        for i in 0..holidays.len() {
587            assert_eq!(holidays[i], holidays2[i]);
588        }
589    }
590
591    #[test]
592    fn test_usexchange_calendar_empty() {
593        let sc = UsExchangeCalendar::with_default_range(false);
594        let c = sc.get_cal();
595        assert!(c.holidays.len() == 0);
596        assert!(c.halfdays.len() == 0);
597        assert!(c.weekdays.len() == 0);
598    }
599
600    #[test]
601    fn test_usexchange_calendar_populated() {
602        let sc = UsExchangeCalendar::with_default_range(true);
603        let c = sc.get_cal();
604        assert!(c.holidays.len() > 0);
605        assert!(c.halfdays.len() > 0);
606        assert!(c.weekdays.len() > 0);
607        assert!(c.is_holiday(Calendar::from_ymd(2021, 1, 1)));
608        assert_eq!(false, c.is_holiday(Calendar::from_ymd(2021, 12, 31)))
609    }
610
611    #[test]
612    fn test_usexchange_calendar_with_new_rule() {
613        // imaginary holiday, let's call it March Madness Day
614        let mut sc = UsExchangeCalendar::with_default_range(false);
615        let holiday = Holiday::MonthWeekday {
616            month: 3,
617            weekday: Weekday::Wed,
618            nth: NthWeek::Third,
619            first: None,
620            last: None,
621            half_check: None,
622        };
623        sc.add_holiday_rule(holiday).populate_cal(None, None);
624        let c = sc.get_cal();
625        assert_eq!(true, c.is_holiday(Calendar::from_ymd(2022, 3, 16)));
626    }
627
628    #[test]
629    fn test_is_trading_date() {
630        let cal = make_cal();
631        assert_eq!(cal.is_business_day(Calendar::from_ymd(2021, 4, 18)), false);
632        assert_eq!(cal.is_business_day(Calendar::from_ymd(2021, 4, 19)), true);
633        assert_eq!(cal.is_business_day(Calendar::from_ymd(2021, 1, 1)), false);
634    }
635
636    #[test]
637    fn test_is_partial_trading_date() {
638        let cal = make_cal();
639        assert_eq!(cal.is_half_holiday(Calendar::from_ymd(2027, 12, 23)), false);
640        assert_eq!(cal.is_half_holiday(Calendar::from_ymd(2026, 7, 2)), false);
641        assert_eq!(cal.is_half_holiday(Calendar::from_ymd(2021, 11, 26)), true);
642        assert_eq!(cal.is_half_holiday(Calendar::from_ymd(2022, 5, 12)), false);
643    }
644
645    #[test]
646    fn test_prev_biz_day() {
647        let cal = make_cal();
648        assert_eq!(
649            cal.prev_biz_day(Calendar::from_ymd(2021, 1, 18)),
650            Calendar::from_ymd(2021, 1, 15)
651        );
652        assert_eq!(
653            cal.prev_biz_day(Calendar::from_ymd(2021, 4, 19)),
654            Calendar::from_ymd(2021, 4, 16)
655        );
656        assert_eq!(
657            cal.prev_biz_day(Calendar::from_ymd(2021, 8, 9)),
658            Calendar::from_ymd(2021, 8, 6)
659        );
660    }
661
662    #[test]
663    fn test_next_biz_day() {
664        let cal = make_cal();
665        assert_eq!(
666            cal.next_biz_day(Calendar::from_ymd(2021, 4, 16)),
667            Calendar::from_ymd(2021, 4, 19)
668        );
669        assert_eq!(
670            cal.next_biz_day(Calendar::from_ymd(2021, 4, 19)),
671            Calendar::from_ymd(2021, 4, 20)
672        );
673        assert_eq!(
674            cal.next_biz_day(Calendar::from_ymd(2021, 4, 2)),
675            Calendar::from_ymd(2021, 4, 5)
676        );
677    }
678}