rateslib/scheduling/frequency/
imm.rs

1#![allow(non_camel_case_types)]
2
3use chrono::prelude::*;
4use chrono::Months;
5use pyo3::exceptions::PyValueError;
6use pyo3::prelude::*;
7use serde::{Deserialize, Serialize};
8use std::cmp::{Eq, PartialEq};
9
10use crate::scheduling::ndt;
11
12/// Specifier for IMM date definitions.
13#[pyclass(module = "rateslib.rs", eq)]
14#[derive(Debug, Copy, Hash, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub enum Imm {
16    /// 3rd Wednesday of March, June, September and December.
17    ///
18    /// Commonly used by STIR futures in northern hemisphere.
19    Wed3_HMUZ = 0,
20    /// 3rd Wednesday of any calendar month.
21    ///
22    /// Commonly used by STIR futures in northern hemisphere.
23    Wed3 = 1,
24    /// 20th day of March, June, September and December.
25    ///
26    /// Commonly used by CDS.
27    Day20_HMUZ = 2,
28    /// 20th day of March and September.
29    ///
30    /// Commonly used by CDS.
31    Day20_HU = 3,
32    /// 20th day of June and December.
33    ///
34    /// Commonly used by CDS.
35    Day20_MZ = 4,
36    /// 20th day of any calendar month.
37    Day20 = 5,
38    /// 2nd Friday of March, June, September and December.
39    ///
40    /// Commonly used by ASX 90 day AUD bank bill futures.
41    Fri2_HMUZ = 6,
42    /// 2nd Friday of any calendar month.
43    ///
44    /// Commonly used by ASX 90 day AUD bank bill futures.
45    Fri2 = 7,
46    /// 1st Wednesday after the 9th of the month in March, June, September and December.
47    ///
48    /// Commonly used by ASX 90 day NZD bank bill futures.
49    Wed1_Post9_HMUZ = 10,
50    /// 1st Wednesday after the 9th of any calendar month.
51    ///
52    /// Commonly used by ASX 90 day NZD bank bill futures.
53    Wed1_Post9 = 11,
54    /// End of any calendar month.
55    Eom = 8,
56    /// February Leap days.
57    Leap = 9,
58    /// Start of any calendar month.
59    Som = 12,
60}
61
62impl Imm {
63    /// Check whether a given date aligns with the IMM date definition.
64    pub fn validate(&self, date: &NaiveDateTime) -> bool {
65        let result = self.from_ym_opt(date.year(), date.month());
66        match result {
67            Ok(val) => *date == val,
68            Err(_) => false,
69        }
70    }
71
72    /// Get an IMM date with the appropriate definition from a given month and year.
73    pub fn from_ym_opt(&self, year: i32, month: u32) -> Result<NaiveDateTime, PyErr> {
74        match self {
75            Imm::Wed3_HMUZ => {
76                if month == 3 || month == 6 || month == 9 || month == 12 {
77                    Imm::Wed3.from_ym_opt(year, month)
78                } else {
79                    Err(PyValueError::new_err("Must be month Mar, Jun, Sep or Dec."))
80                }
81            }
82            Imm::Fri2_HMUZ => {
83                if month == 3 || month == 6 || month == 9 || month == 12 {
84                    Imm::Fri2.from_ym_opt(year, month)
85                } else {
86                    Err(PyValueError::new_err("Must be month Mar, Jun, Sep or Dec."))
87                }
88            }
89            Imm::Wed1_Post9_HMUZ => {
90                if month == 3 || month == 6 || month == 9 || month == 12 {
91                    Imm::Wed1_Post9.from_ym_opt(year, month)
92                } else {
93                    Err(PyValueError::new_err("Must be month Mar, Jun, Sep or Dec."))
94                }
95            }
96            Imm::Wed3 => {
97                let w = ndt(year, month, 1).weekday() as u32;
98                let r = if w <= 2 { 17 - w } else { 24 - w };
99                Ok(ndt(year, month, r))
100            }
101            Imm::Fri2 => {
102                let w = ndt(year, month, 1).weekday() as u32;
103                let r = if w <= 4 { 12 - w } else { 19 - w };
104                Ok(ndt(year, month, r))
105            }
106            Imm::Wed1_Post9 => {
107                let w = ndt(year, month, 1).weekday() as u32;
108                let r = if w <= 0 { 10 - w } else { 17 - w };
109                Ok(ndt(year, month, r))
110            }
111            Imm::Day20_HMUZ => {
112                if month == 3 || month == 6 || month == 9 || month == 12 {
113                    Ok(ndt(year, month, 20))
114                } else {
115                    Err(PyValueError::new_err("Must be month Mar, Jun, Sep or Dec."))
116                }
117            }
118            Imm::Day20_HU => {
119                if month == 3 || month == 9 {
120                    Ok(ndt(year, month, 20))
121                } else {
122                    Err(PyValueError::new_err("Must be month Mar, or Sep."))
123                }
124            }
125            Imm::Day20_MZ => {
126                if month == 6 || month == 12 {
127                    Ok(ndt(year, month, 20))
128                } else {
129                    Err(PyValueError::new_err("Must be month Jun, or Dec."))
130                }
131            }
132            Imm::Day20 => Ok(ndt(year, month, 20)),
133            Imm::Eom => {
134                let mut day = 31;
135                let mut date = NaiveDate::from_ymd_opt(year, month, day);
136                while date == None {
137                    day = day - 1;
138                    date = NaiveDate::from_ymd_opt(year, month, day);
139                    if day == 0 {
140                        return Err(PyValueError::new_err("`year` or `month` out of range."));
141                    }
142                }
143                Ok(date.unwrap().and_hms_opt(0, 0, 0).unwrap())
144            }
145            Imm::Som => {
146                let date = NaiveDate::from_ymd_opt(year, month, 1);
147                match date {
148                    Some(d) => Ok(d.and_hms_opt(0, 0, 0).unwrap()),
149                    None => return Err(PyValueError::new_err("`year` or `month` out of range.")),
150                }
151            }
152            Imm::Leap => {
153                if month != 2 {
154                    Err(PyValueError::new_err("Leap is only in `month`:2."))
155                } else {
156                    let d = NaiveDate::from_ymd_opt(year, 2, 29);
157                    match d {
158                        None => Err(PyValueError::new_err("No Leap in given `year`.")),
159                        Some(val) => Ok(val.and_hms_opt(0, 0, 0).unwrap()),
160                    }
161                }
162            }
163        }
164    }
165
166    /// Get the IMM date that follows the given ``date``.
167    pub fn next(&self, date: &NaiveDateTime) -> NaiveDateTime {
168        let mut sample = *date;
169        let mut result = self.from_ym_opt(date.year(), date.month());
170        loop {
171            match result {
172                Ok(v) if v > *date => return v,
173                _ => {
174                    sample = sample + Months::new(1);
175                    result = self.from_ym_opt(sample.year(), sample.month());
176                }
177            }
178        }
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn imm_date_determination() {
188        let options: Vec<(Imm, NaiveDateTime, bool)> = vec![
189            (Imm::Wed3_HMUZ, ndt(2000, 3, 15), true),
190            (Imm::Wed3_HMUZ, ndt(2000, 3, 22), false),
191            (Imm::Wed3_HMUZ, ndt(2000, 3, 8), false),
192            (Imm::Wed3_HMUZ, ndt(2000, 2, 21), false),
193            (Imm::Wed3, ndt(2024, 2, 21), true),
194            (Imm::Wed3, ndt(2000, 3, 15), true),
195            (Imm::Wed3, ndt(2025, 3, 19), true),
196            (Imm::Wed3, ndt(2025, 3, 18), false),
197            (Imm::Day20_HMUZ, ndt(2000, 2, 21), false),
198            (Imm::Day20_HMUZ, ndt(2000, 2, 20), false),
199            (Imm::Day20_HMUZ, ndt(2000, 3, 20), true),
200            (Imm::Day20_HU, ndt(2000, 3, 20), true),
201            (Imm::Day20_HU, ndt(2000, 6, 20), false),
202            (Imm::Day20_MZ, ndt(2000, 3, 20), false),
203            (Imm::Day20_MZ, ndt(2000, 6, 20), true),
204            (Imm::Fri2, ndt(2024, 2, 9), true),
205            (Imm::Fri2, ndt(2024, 12, 13), true),
206            (Imm::Wed1_Post9, ndt(2025, 9, 10), true),
207            (Imm::Wed1_Post9, ndt(2026, 9, 16), true),
208            (Imm::Som, ndt(2025, 9, 1), true),
209            (Imm::Som, ndt(2026, 9, 16), false),
210        ];
211        for option in options {
212            assert_eq!(option.2, option.0.validate(&option.1));
213        }
214    }
215
216    #[test]
217    fn next_check() {
218        let options: Vec<(Imm, NaiveDateTime, NaiveDateTime)> = vec![
219            (Imm::Wed3_HMUZ, ndt(2024, 3, 20), ndt(2024, 6, 19)),
220            (Imm::Wed3_HMUZ, ndt(2024, 3, 19), ndt(2024, 3, 20)),
221            (Imm::Wed3, ndt(2024, 3, 21), ndt(2024, 4, 17)),
222            (Imm::Day20_HU, ndt(2024, 3, 21), ndt(2024, 9, 20)),
223            (Imm::Leap, ndt(2022, 1, 1), ndt(2024, 2, 29)),
224            (Imm::Som, ndt(2022, 1, 1), ndt(2022, 2, 1)),
225        ];
226        for option in options {
227            assert_eq!(option.2, option.0.next(&option.1));
228        }
229    }
230
231    #[test]
232    fn test_is_eom() {
233        assert_eq!(true, Imm::Eom.validate(&ndt(2025, 3, 31)));
234        assert_eq!(false, Imm::Eom.validate(&ndt(2025, 3, 30)));
235    }
236
237    #[test]
238    fn test_get_from() {
239        assert_eq!(ndt(2022, 2, 28), Imm::Eom.from_ym_opt(2022, 2).unwrap());
240        assert_eq!(ndt(2024, 2, 29), Imm::Eom.from_ym_opt(2024, 2).unwrap());
241        assert_eq!(ndt(2022, 4, 30), Imm::Eom.from_ym_opt(2022, 4).unwrap());
242        assert_eq!(ndt(2022, 3, 31), Imm::Eom.from_ym_opt(2022, 3).unwrap());
243        assert_eq!(ndt(2024, 2, 29), Imm::Leap.from_ym_opt(2024, 2).unwrap());
244        assert_eq!(ndt(2024, 2, 1), Imm::Som.from_ym_opt(2024, 2).unwrap());
245        assert!(Imm::Leap.from_ym_opt(2022, 2).is_err());
246    }
247}