Skip to main content

rustyqlib/core/
daycount.rs

1use chrono::{Datelike, NaiveDate};
2use serde::{Deserialize, Serialize};
3
4/// Day count conventions used to convert a pair of dates into a year fraction.
5///
6/// This is the single bridge between calendar dates and the year-fraction
7/// times used by [`crate::core::curves::YieldCurve`]; instruments carry their
8/// own convention and curves carry theirs.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
10pub enum DayCountConvention {
11    /// Actual days / 365 (fixed)
12    #[default]
13    #[serde(alias = "Act/365", alias = "ACT/365", alias = "act365", alias = "A365")]
14    Act365,
15    /// Actual days / 360
16    #[serde(alias = "Act/360", alias = "ACT/360", alias = "act360", alias = "A360")]
17    Act360,
18    /// 30/360 US (bond basis)
19    #[serde(alias = "30/360", alias = "thirty360")]
20    Thirty360,
21}
22
23impl DayCountConvention {
24    /// Year fraction from `start` to `end` under this convention.
25    /// Negative if `end` is before `start`.
26    pub fn year_fraction(&self, start: NaiveDate, end: NaiveDate) -> f64 {
27        match self {
28            DayCountConvention::Act365 => (end - start).num_days() as f64 / 365.0,
29            DayCountConvention::Act360 => (end - start).num_days() as f64 / 360.0,
30            DayCountConvention::Thirty360 => {
31                let (y1, m1, mut d1) = (start.year(), start.month() as i64, start.day() as i64);
32                let (y2, m2, mut d2) = (end.year(), end.month() as i64, end.day() as i64);
33                if d1 == 31 {
34                    d1 = 30;
35                }
36                if d2 == 31 && d1 == 30 {
37                    d2 = 30;
38                }
39                (360 * (y2 - y1) as i64 + 30 * (m2 - m1) + (d2 - d1)) as f64 / 360.0
40            }
41        }
42    }
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48
49    fn d(y: i32, m: u32, day: u32) -> NaiveDate {
50        NaiveDate::from_ymd_opt(y, m, day).unwrap()
51    }
52
53    #[test]
54    fn act365_one_year() {
55        let yf = DayCountConvention::Act365.year_fraction(d(2026, 1, 1), d(2027, 1, 1));
56        assert!((yf - 1.0).abs() < 1e-12);
57    }
58
59    #[test]
60    fn act360_ninety_days() {
61        let yf = DayCountConvention::Act360.year_fraction(d(2026, 1, 1), d(2026, 4, 1));
62        assert!((yf - 90.0 / 360.0).abs() < 1e-12);
63    }
64
65    #[test]
66    fn thirty360_half_year_month_ends() {
67        // Jan 31 -> Jul 31 is exactly 0.5 under 30/360 US
68        let yf = DayCountConvention::Thirty360.year_fraction(d(2026, 1, 31), d(2026, 7, 31));
69        assert!((yf - 0.5).abs() < 1e-12);
70    }
71}