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/// An IMM date definition.
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}
59
60impl Imm {
61    /// Check whether a given date aligns with the IMM date definition.
62    pub fn validate(&self, date: &NaiveDateTime) -> bool {
63        let result = self.from_ym_opt(date.year(), date.month());
64        match result {
65            Ok(val) => *date == val,
66            Err(_) => false,
67        }
68    }
69
70    /// Get an IMM date with the appropriate definition from a given month and year.
71    pub fn from_ym_opt(&self, year: i32, month: u32) -> Result<NaiveDateTime, PyErr> {
72        match self {
73            Imm::Wed3_HMUZ => {
74                if month == 3 || month == 6 || month == 9 || month == 12 {
75                    Imm::Wed3.from_ym_opt(year, month)
76                } else {
77                    Err(PyValueError::new_err("Must be month Mar, Jun, Sep or Dec."))
78                }
79            }
80            Imm::Fri2_HMUZ => {
81                if month == 3 || month == 6 || month == 9 || month == 12 {
82                    Imm::Fri2.from_ym_opt(year, month)
83                } else {
84                    Err(PyValueError::new_err("Must be month Mar, Jun, Sep or Dec."))
85                }
86            }
87            Imm::Wed1_Post9_HMUZ => {
88                if month == 3 || month == 6 || month == 9 || month == 12 {
89                    Imm::Wed1_Post9.from_ym_opt(year, month)
90                } else {
91                    Err(PyValueError::new_err("Must be month Mar, Jun, Sep or Dec."))
92                }
93            }
94            Imm::Wed3 => {
95                let w = ndt(year, month, 1).weekday() as u32;
96                let r = if w <= 2 { 17 - w } else { 24 - w };
97                Ok(ndt(year, month, r))
98            }
99            Imm::Fri2 => {
100                let w = ndt(year, month, 1).weekday() as u32;
101                let r = if w <= 4 { 12 - w } else { 19 - w };
102                Ok(ndt(year, month, r))
103            }
104            Imm::Wed1_Post9 => {
105                let w = ndt(year, month, 1).weekday() as u32;
106                let r = if w <= 0 { 10 - w } else { 17 - w };
107                Ok(ndt(year, month, r))
108            }
109            Imm::Day20_HMUZ => {
110                if month == 3 || month == 6 || month == 9 || month == 12 {
111                    Ok(ndt(year, month, 20))
112                } else {
113                    Err(PyValueError::new_err("Must be month Mar, Jun, Sep or Dec."))
114                }
115            }
116            Imm::Day20_HU => {
117                if month == 3 || month == 9 {
118                    Ok(ndt(year, month, 20))
119                } else {
120                    Err(PyValueError::new_err("Must be month Mar, or Sep."))
121                }
122            }
123            Imm::Day20_MZ => {
124                if month == 6 || month == 12 {
125                    Ok(ndt(year, month, 20))
126                } else {
127                    Err(PyValueError::new_err("Must be month Jun, or Dec."))
128                }
129            }
130            Imm::Day20 => Ok(ndt(year, month, 20)),
131            Imm::Eom => {
132                let mut day = 31;
133                let mut date = NaiveDate::from_ymd_opt(year, month, day);
134                while date == None {
135                    day = day - 1;
136                    date = NaiveDate::from_ymd_opt(year, month, day);
137                    if day == 0 {
138                        return Err(PyValueError::new_err("`year` or `month` out of range."));
139                    }
140                }
141                Ok(date.unwrap().and_hms_opt(0, 0, 0).unwrap())
142            }
143            Imm::Leap => {
144                if month != 2 {
145                    Err(PyValueError::new_err("Leap is only in `month`:2."))
146                } else {
147                    let d = NaiveDate::from_ymd_opt(year, 2, 29);
148                    match d {
149                        None => Err(PyValueError::new_err("No Leap in given `year`.")),
150                        Some(val) => Ok(val.and_hms_opt(0, 0, 0).unwrap()),
151                    }
152                }
153            }
154        }
155    }
156
157    /// Get the IMM date that follows the given ``date``.
158    pub fn next(&self, date: &NaiveDateTime) -> NaiveDateTime {
159        let mut sample = *date;
160        let mut result = self.from_ym_opt(date.year(), date.month());
161        loop {
162            match result {
163                Ok(v) if v > *date => return v,
164                _ => {
165                    sample = sample + Months::new(1);
166                    result = self.from_ym_opt(sample.year(), sample.month());
167                }
168            }
169        }
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn imm_date_determination() {
179        let options: Vec<(Imm, NaiveDateTime, bool)> = vec![
180            (Imm::Wed3_HMUZ, ndt(2000, 3, 15), true),
181            (Imm::Wed3_HMUZ, ndt(2000, 3, 22), false),
182            (Imm::Wed3_HMUZ, ndt(2000, 3, 8), false),
183            (Imm::Wed3_HMUZ, ndt(2000, 2, 21), false),
184            (Imm::Wed3, ndt(2024, 2, 21), true),
185            (Imm::Wed3, ndt(2000, 3, 15), true),
186            (Imm::Wed3, ndt(2025, 3, 19), true),
187            (Imm::Wed3, ndt(2025, 3, 18), false),
188            (Imm::Day20_HMUZ, ndt(2000, 2, 21), false),
189            (Imm::Day20_HMUZ, ndt(2000, 2, 20), false),
190            (Imm::Day20_HMUZ, ndt(2000, 3, 20), true),
191            (Imm::Day20_HU, ndt(2000, 3, 20), true),
192            (Imm::Day20_HU, ndt(2000, 6, 20), false),
193            (Imm::Day20_MZ, ndt(2000, 3, 20), false),
194            (Imm::Day20_MZ, ndt(2000, 6, 20), true),
195            (Imm::Fri2, ndt(2024, 2, 9), true),
196            (Imm::Fri2, ndt(2024, 12, 13), true),
197            (Imm::Wed1_Post9, ndt(2025, 9, 10), true),
198            (Imm::Wed1_Post9, ndt(2026, 9, 16), true),
199        ];
200        for option in options {
201            assert_eq!(option.2, option.0.validate(&option.1));
202        }
203    }
204
205    #[test]
206    fn next_check() {
207        let options: Vec<(Imm, NaiveDateTime, NaiveDateTime)> = vec![
208            (Imm::Wed3_HMUZ, ndt(2024, 3, 20), ndt(2024, 6, 19)),
209            (Imm::Wed3_HMUZ, ndt(2024, 3, 19), ndt(2024, 3, 20)),
210            (Imm::Wed3, ndt(2024, 3, 21), ndt(2024, 4, 17)),
211            (Imm::Day20_HU, ndt(2024, 3, 21), ndt(2024, 9, 20)),
212            (Imm::Leap, ndt(2022, 1, 1), ndt(2024, 2, 29)),
213        ];
214        for option in options {
215            assert_eq!(option.2, option.0.next(&option.1));
216        }
217    }
218
219    #[test]
220    fn test_is_eom() {
221        assert_eq!(true, Imm::Eom.validate(&ndt(2025, 3, 31)));
222        assert_eq!(false, Imm::Eom.validate(&ndt(2025, 3, 30)));
223    }
224
225    #[test]
226    fn test_get_from() {
227        assert_eq!(ndt(2022, 2, 28), Imm::Eom.from_ym_opt(2022, 2).unwrap());
228        assert_eq!(ndt(2024, 2, 29), Imm::Eom.from_ym_opt(2024, 2).unwrap());
229        assert_eq!(ndt(2022, 4, 30), Imm::Eom.from_ym_opt(2022, 4).unwrap());
230        assert_eq!(ndt(2022, 3, 31), Imm::Eom.from_ym_opt(2022, 3).unwrap());
231        assert_eq!(ndt(2024, 2, 29), Imm::Leap.from_ym_opt(2024, 2).unwrap());
232        assert!(Imm::Leap.from_ym_opt(2022, 2).is_err());
233    }
234}