use crate::error::ParamError;
use crate::market::units::{Maturity, TotalVariance};
#[derive(Debug, Clone, PartialEq)]
pub struct TermStructure(Vec<(Maturity, TotalVariance)>);
impl TermStructure {
pub fn new(knots: Vec<(Maturity, TotalVariance)>) -> Result<Self, ParamError> {
if knots.is_empty() {
return Err(ParamError::EmptyCollection {
name: "term structure",
});
}
for &(_, theta) in &knots {
if theta.get() <= 0.0 {
return Err(ParamError::NonPositiveTheta { theta: theta.get() });
}
}
for (index, pair) in knots.windows(2).enumerate() {
if pair[1].0 <= pair[0].0 {
return Err(ParamError::NotStrictlyIncreasing {
name: "maturity",
index: index + 1,
previous: pair[0].0.get(),
value: pair[1].0.get(),
});
}
if pair[1].1 < pair[0].1 {
return Err(ParamError::DecreasingAtmVariance {
index: index + 1,
previous: pair[0].1.get(),
value: pair[1].1.get(),
});
}
}
Ok(Self(knots))
}
#[must_use]
pub fn knots(&self) -> &[(Maturity, TotalVariance)] {
&self.0
}
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl TryFrom<Vec<(f64, f64)>> for TermStructure {
type Error = ParamError;
fn try_from(knots: Vec<(f64, f64)>) -> Result<Self, Self::Error> {
let typed = knots
.into_iter()
.map(|(t, theta)| Ok((Maturity::new(t)?, TotalVariance::new(theta)?)))
.collect::<Result<Vec<_>, ParamError>>()?;
Self::new(typed)
}
}
#[cfg(test)]
#[allow(clippy::expect_used)] mod tests {
use super::*;
#[test]
fn rejects_unordered_and_decreasing_knots() {
assert!(TermStructure::try_from(vec![(0.5, 0.02), (1.0, 0.04)]).is_ok());
assert!(matches!(
TermStructure::try_from(vec![(1.0, 0.04), (0.5, 0.02)]),
Err(ParamError::NotStrictlyIncreasing { .. })
));
assert!(matches!(
TermStructure::try_from(vec![(0.5, 0.04), (1.0, 0.02)]),
Err(ParamError::DecreasingAtmVariance { .. })
));
}
}