finquant/time/
imm.rs

1use chrono::{Datelike, NaiveDate, Weekday};
2use std::str::FromStr;
3use strum::IntoEnumIterator;
4use strum_macros::{Display, EnumIter, EnumString};
5
6/// IMM Month Codes.
7/// https://www.cmegroup.com/month-codes.html
8#[repr(u16)]
9#[derive(EnumIter, EnumString, Display, PartialEq, Debug)]
10pub enum IMMMonth {
11    F = 1,
12    G = 2,
13    H = 3,
14    J = 4,
15    K = 5,
16    M = 6,
17    N = 7,
18    Q = 8,
19    U = 9,
20    V = 10,
21    X = 11,
22    Z = 12,
23}
24
25/// IMM Related.
26pub struct IMM;
27
28impl IMM {
29    /// Check is IMM Date.
30    /// https://en.wikipedia.org/wiki/IMM_dates
31    /// The IMM dates are the four quarterly dates of each year as scheduled maturity date.
32    /// The dates are the third Wednesday of March, June, September and December
33    /// (i.e., between the 15th and 21st, whichever such day is a Wednesday).
34    pub fn is_imm_date(&self, date: NaiveDate, main_cycle: bool) -> bool {
35        if date.weekday() != Weekday::Wed {
36            return false;
37        }
38
39        let d = date.day();
40        if !(15..=21).contains(&d) {
41            return false;
42        }
43
44        if !main_cycle {
45            return true;
46        }
47
48        matches!(date.month(), 3 | 6 | 9 | 12)
49    }
50
51    /// IMM Codes are constructed by IMMMonth + year.
52    pub fn is_imm_code(&self, imm_code: String, main_cycle: bool) -> bool {
53        if imm_code.len() != 2 {
54            return false;
55        }
56
57        let imm_year = imm_code
58            .chars()
59            .nth(1)
60            .expect("already asserted length of 2");
61
62        if !"0123456789".contains(imm_year) {
63            return false;
64        }
65
66        let str = if main_cycle {
67            "hmzuHMZU".to_string()
68        } else {
69            "fghjkmnquvxzFGHJKMNQUVXZ".to_string()
70        };
71
72        let imm_month = imm_code
73            .chars()
74            .nth(0)
75            .expect("already asserted length of 2");
76
77        if !str.contains(imm_month) {
78            return false;
79        }
80
81        true
82    }
83
84    /// Convert a valid date to IMM code.
85    pub fn code(&self, date: NaiveDate) -> Option<String> {
86        if !self.is_imm_date(date, false) {
87            None
88        } else {
89            let y = date.year() % 10;
90            let mut month = IMMMonth::iter()
91                .nth((date.month() - 1) as usize)
92                .expect("month is within range")
93                .to_string();
94            month.push_str(y.to_string().as_str());
95            Some(month)
96        }
97    }
98
99    /// IMM Code to maturity date.
100    pub fn date(&self, imm_code: String, ref_date: Option<NaiveDate>) -> Option<NaiveDate> {
101        if !self.is_imm_code(imm_code.clone(), false) {
102            None
103        } else {
104            let ref_date = ref_date.unwrap_or(chrono::offset::Utc::now().date_naive());
105            let month = imm_code.chars().nth(0).unwrap();
106            let mut year = imm_code.chars().nth(1).unwrap().to_digit(10).unwrap() as i32;
107            let imm_month = IMMMonth::from_str(&month.to_string()).unwrap() as u32;
108            if year == 0 && ref_date.year() <= 1909 {
109                year += 10
110            }
111            let ref_year = ref_date.year() % 10;
112            year += ref_date.year() - ref_year;
113            let result =
114                self.next_date(NaiveDate::from_ymd_opt(year, imm_month, 1).unwrap(), false);
115            if result < ref_date {
116                Some(self.next_date(
117                    NaiveDate::from_ymd_opt(year + 10, imm_month, 1).unwrap(),
118                    false,
119                ))
120            } else {
121                Some(result)
122            }
123        }
124    }
125
126    /// Next date.
127    pub fn next_date(&self, date: NaiveDate, main_cycle: bool) -> NaiveDate {
128        let mut month = date.month();
129        let mut year = date.year();
130        let offset = if main_cycle { 3 } else { 1 };
131        let mut skip_months = offset - (date.month() % offset);
132        if skip_months != offset || date.day() > 21 {
133            skip_months += date.month();
134            if skip_months > 12 {
135                month -= 12;
136                year += 1;
137            }
138        }
139        let mut result = self.nth_weekday(3, Weekday::Wed, month, year).unwrap();
140        if result <= date {
141            result = self.next_date(
142                NaiveDate::from_ymd_opt(year, month, 22).unwrap(),
143                main_cycle,
144            );
145        }
146        result
147    }
148
149    fn nth_weekday(&self, nth: i32, day_of_week: Weekday, m: u32, y: i32) -> Option<NaiveDate> {
150        if !(0..=6).contains(&nth) {
151            None
152        } else {
153            let first = NaiveDate::from_ymd_opt(y, m, 1).unwrap().weekday();
154            let skip = nth
155                - (if day_of_week.num_days_from_monday() >= first.num_days_from_monday() {
156                    1
157                } else {
158                    0
159                });
160            NaiveDate::from_ymd_opt(
161                y,
162                m,
163                1 + day_of_week.num_days_from_monday() + skip as u32 * 7
164                    - first.num_days_from_monday(),
165            )
166        }
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::{IMM, IMMMonth};
173    use chrono::NaiveDate;
174    use std::str::FromStr;
175    use strum::IntoEnumIterator;
176
177    #[test]
178    fn test_imm_month() {
179        assert_eq!(IMMMonth::iter().nth(5).unwrap().to_string(), "M");
180        assert_eq!(IMMMonth::from_str("F").unwrap() as u16, 1);
181    }
182    #[test]
183    fn test_imm_code() {
184        assert_eq!(IMM.is_imm_code("more_than_2".to_string(), false), false);
185        assert_eq!(IMM.is_imm_code("1".to_string(), false), false);
186        assert_eq!(IMM.is_imm_code("".to_string(), false), false);
187        assert_eq!(IMM.is_imm_code("1F".to_string(), false), false);
188        assert_eq!(IMM.is_imm_code("F1".to_string(), true), false);
189        assert_eq!(IMM.is_imm_code("F1".to_string(), false), true);
190    }
191
192    #[test]
193    fn test_generate_code() {
194        assert_eq!(
195            IMM.code(NaiveDate::from_ymd_opt(2023, 9, 20).unwrap()),
196            Some(String::from("U3".to_string()))
197        );
198    }
199
200    #[test]
201    fn test_imm_code_to_date() {
202        assert_eq!(
203            IMM.date("X3".to_string(), NaiveDate::from_ymd_opt(2023, 10, 29)),
204            NaiveDate::from_ymd_opt(2023, 11, 15)
205        );
206        assert_eq!(
207            IMM.date("Z3".to_string(), NaiveDate::from_ymd_opt(2023, 10, 29)),
208            NaiveDate::from_ymd_opt(2023, 12, 20)
209        );
210        assert_eq!(
211            IMM.date("F4".to_string(), NaiveDate::from_ymd_opt(2023, 10, 29)),
212            NaiveDate::from_ymd_opt(2024, 1, 17)
213        );
214        assert_eq!(
215            IMM.date("G4".to_string(), NaiveDate::from_ymd_opt(2023, 10, 29)),
216            NaiveDate::from_ymd_opt(2024, 2, 21)
217        );
218        assert_eq!(
219            IMM.date("H4".to_string(), NaiveDate::from_ymd_opt(2023, 10, 29)),
220            NaiveDate::from_ymd_opt(2024, 3, 20)
221        );
222        assert_eq!(
223            IMM.date("J4".to_string(), NaiveDate::from_ymd_opt(2023, 10, 29)),
224            NaiveDate::from_ymd_opt(2024, 4, 17)
225        );
226        assert_eq!(
227            IMM.date("M4".to_string(), NaiveDate::from_ymd_opt(2023, 10, 29)),
228            NaiveDate::from_ymd_opt(2024, 6, 19)
229        );
230        assert_eq!(
231            IMM.date("U4".to_string(), NaiveDate::from_ymd_opt(2023, 10, 29)),
232            NaiveDate::from_ymd_opt(2024, 9, 18)
233        );
234        assert_eq!(
235            IMM.date("Z4".to_string(), NaiveDate::from_ymd_opt(2023, 10, 29)),
236            NaiveDate::from_ymd_opt(2024, 12, 18)
237        );
238        assert_eq!(
239            IMM.date("H5".to_string(), NaiveDate::from_ymd_opt(2023, 10, 29)),
240            NaiveDate::from_ymd_opt(2025, 3, 19)
241        );
242        assert_eq!(
243            IMM.date("M5".to_string(), NaiveDate::from_ymd_opt(2023, 10, 29)),
244            NaiveDate::from_ymd_opt(2025, 6, 18)
245        );
246        assert_eq!(
247            IMM.date("U5".to_string(), NaiveDate::from_ymd_opt(2023, 10, 29)),
248            NaiveDate::from_ymd_opt(2025, 9, 17)
249        );
250    }
251}