use super::ScheduleError;
use crate::error::IdentifierError;
use crate::identifier::{Entry, Identifiers};
use crate::time::{Date, Period, TimeUnit};
use std::fmt;
use std::str::FromStr;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[expect(
clippy::exhaustive_enums,
reason = "closed two-regime quoted CDS maturity contract"
)]
pub enum CdsMaturityConvention {
Quarterly2009,
Semiannual2015,
}
impl CdsMaturityConvention {
pub const ALL: [Self; 2] = [Self::Quarterly2009, Self::Semiannual2015];
#[must_use]
pub const fn canonical_identifier(self) -> &'static str {
match self {
Self::Quarterly2009 => "Quarterly2009",
Self::Semiannual2015 => "Semiannual2015",
}
}
}
const CONVENTION_IDS: Identifiers<CdsMaturityConvention> = Identifiers::new(
"CDS maturity convention",
&[
Entry {
value: CdsMaturityConvention::Quarterly2009,
canonical: "Quarterly2009",
aliases: &[],
},
Entry {
value: CdsMaturityConvention::Semiannual2015,
canonical: "Semiannual2015",
aliases: &[],
},
],
&[],
);
impl fmt::Display for CdsMaturityConvention {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.canonical_identifier())
}
}
impl FromStr for CdsMaturityConvention {
type Err = IdentifierError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
CONVENTION_IDS.parse(text)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[expect(
clippy::exhaustive_enums,
reason = "closed successful quoted-maturity outcome contract"
)]
pub enum CdsMaturityOutcome {
Date(Date),
Matured,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct CdsMaturityResolution {
trade_date: Date,
quoted_length: i32,
quoted_unit: TimeUnit,
convention: CdsMaturityConvention,
outcome: CdsMaturityOutcome,
}
impl CdsMaturityResolution {
#[must_use]
pub const fn trade_date(&self) -> Date {
self.trade_date
}
#[must_use]
pub const fn quoted_tenor(&self) -> Period {
Period::new(self.quoted_length, self.quoted_unit)
}
#[must_use]
pub const fn convention(&self) -> CdsMaturityConvention {
self.convention
}
#[must_use]
pub const fn outcome(&self) -> CdsMaturityOutcome {
self.outcome
}
}
pub fn resolve_cds_maturity(
trade_date: Date,
quoted_tenor: Period,
convention: CdsMaturityConvention,
) -> Result<CdsMaturityResolution, ScheduleError> {
let quoted_length = quoted_tenor.length();
let quoted_unit = quoted_tenor.unit();
let quoted_months = match (quoted_length, quoted_unit) {
(length, TimeUnit::Months) if length >= 0 && length % 3 == 0 => i64::from(length),
(length, TimeUnit::Years) if length >= 0 => i64::from(length) * 12,
_ => {
return Err(ScheduleError::InvalidCdsTenor {
trade_date,
tenor: quoted_tenor,
convention,
});
}
};
let (year, month, day) = trade_date.to_ymd();
let trade_month = i64::from(year - 1) * 12 + i64::from(month - 1);
let cutoff = trade_month - i64::from(day < 20);
let step = match convention {
CdsMaturityConvention::Quarterly2009 => 3,
CdsMaturityConvention::Semiannual2015 => 6,
};
let base = cutoff - (cutoff - 2).rem_euclid(step) + 3;
let outcome = if quoted_months == 0 && (base, 20) <= (trade_month, day) {
CdsMaturityOutcome::Matured
} else {
let target = base + quoted_months;
let resolved = trade_date
.checked_add_months(target - trade_month)
.and_then(|date| {
let (target_year, target_month, _) = date.to_ymd();
Date::from_ymd(target_year, target_month, 20)
})
.map_err(|cause| ScheduleError::CdsMaturityDate {
trade_date,
tenor: quoted_tenor,
convention,
cause,
})?;
CdsMaturityOutcome::Date(resolved)
};
Ok(CdsMaturityResolution {
trade_date,
quoted_length,
quoted_unit,
convention,
outcome,
})
}