use core::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TypeError {
InvalidDate {
year: i32,
month: u32,
day: u32,
},
NonPositiveRange,
NonFinite {
name: &'static str,
},
InvalidTenor {
reason: &'static str,
},
}
impl fmt::Display for TypeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidDate { year, month, day } => {
write!(f, "invalid calendar date: {year:04}-{month:02}-{day:02}")
}
Self::NonPositiveRange => write!(f, "day-count range must be strictly positive"),
Self::NonFinite { name } => write!(f, "input {name} must be a finite number"),
Self::InvalidTenor { reason } => write!(f, "invalid tenor: {reason}"),
}
}
}
impl std::error::Error for TypeError {}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CurveError {
TooFewNodes {
found: usize,
},
NodesNotIncreasing {
at_index: usize,
},
NonPositiveDiscount {
at_index: usize,
value: f64,
},
AnchorNotUnit,
InvalidTime {
t: f64,
},
Type(TypeError),
DuplicateNode {
t: f64,
},
}
impl fmt::Display for CurveError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooFewNodes { found } => {
write!(f, "curve needs at least two nodes, found {found}")
}
Self::NodesNotIncreasing { at_index } => {
write!(f, "node times not strictly increasing at index {at_index}")
}
Self::NonPositiveDiscount { at_index, value } => {
write!(
f,
"discount factor at node {at_index} must be positive, got {value}"
)
}
Self::AnchorNotUnit => write!(f, "anchor node must be (t=0, D=1)"),
Self::InvalidTime { t } => write!(f, "invalid time t = {t}"),
Self::Type(e) => write!(f, "type error in curve query: {e}"),
Self::DuplicateNode { t } => write!(f, "duplicate node at t = {t}"),
}
}
}
impl std::error::Error for CurveError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Type(e) => Some(e),
_ => None,
}
}
}
impl From<TypeError> for CurveError {
fn from(e: TypeError) -> Self {
Self::Type(e)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BootstrapError {
InstrumentsNotOrdered {
at_index: usize,
},
NonIncreasingAnchor {
at_index: usize,
},
LegDidNotConverge {
at_index: usize,
residual: f64,
},
NoBracket {
at_index: usize,
},
InvalidInstrument {
at_index: usize,
reason: &'static str,
},
Curve(CurveError),
Type(TypeError),
}
impl fmt::Display for BootstrapError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InstrumentsNotOrdered { at_index } => {
write!(f, "instruments not ordered at index {at_index}")
}
Self::NonIncreasingAnchor { at_index } => {
write!(
f,
"instrument anchor not strictly increasing at index {at_index}"
)
}
Self::LegDidNotConverge { at_index, residual } => {
write!(
f,
"bootstrap leg {at_index} did not converge: residual {residual:e}"
)
}
Self::NoBracket { at_index } => {
write!(f, "bootstrap leg {at_index} could not bracket a root")
}
Self::InvalidInstrument { at_index, reason } => {
write!(f, "invalid instrument at index {at_index}: {reason}")
}
Self::Curve(e) => write!(f, "curve error during bootstrap: {e}"),
Self::Type(e) => write!(f, "type error during bootstrap: {e}"),
}
}
}
impl std::error::Error for BootstrapError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Curve(e) => Some(e),
Self::Type(e) => Some(e),
_ => None,
}
}
}
impl From<TypeError> for BootstrapError {
fn from(e: TypeError) -> Self {
Self::Type(e)
}
}
impl From<CurveError> for BootstrapError {
fn from(e: CurveError) -> Self {
Self::Curve(e)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn type_error_display_invalid_date() {
let err = TypeError::InvalidDate {
year: 2023,
month: 2,
day: 30,
};
assert_eq!(format!("{err}"), "invalid calendar date: 2023-02-30");
}
#[test]
fn type_error_display_non_positive_range() {
let err = TypeError::NonPositiveRange;
assert!(format!("{err}").contains("positive"));
}
#[test]
fn type_error_display_non_finite() {
let err = TypeError::NonFinite { name: "rate" };
assert!(format!("{err}").contains("rate"));
assert!(format!("{err}").contains("finite"));
}
#[test]
fn type_error_display_invalid_tenor() {
let err = TypeError::InvalidTenor {
reason: "Business252 requires a calendar",
};
assert!(format!("{err}").contains("Business252"));
}
#[test]
fn type_error_is_error_trait() {
let err: &dyn std::error::Error = &TypeError::NonPositiveRange;
assert!(err.source().is_none());
}
#[test]
fn type_error_copy_eq_hash() {
let err = TypeError::NonFinite { name: "rate" };
let copy = err;
assert_eq!(err, copy);
let mut set = std::collections::HashSet::new();
set.insert(err);
assert!(set.contains(©));
}
#[test]
fn type_error_debug() {
assert!(format!("{:?}", TypeError::NonPositiveRange).contains("NonPositiveRange"));
}
#[test]
fn curve_error_display_all_variants() {
assert!(format!("{}", CurveError::TooFewNodes { found: 1 }).contains("two nodes"));
assert!(format!("{}", CurveError::NodesNotIncreasing { at_index: 4 }).contains('4'));
assert!(
format!(
"{}",
CurveError::NonPositiveDiscount {
at_index: 2,
value: -0.5,
}
)
.contains("-0.5")
);
assert!(format!("{}", CurveError::AnchorNotUnit).contains("anchor"));
assert!(format!("{}", CurveError::InvalidTime { t: -1.0 }).contains("-1"));
assert!(format!("{}", CurveError::DuplicateNode { t: 0.5 }).contains("0.5"));
assert!(
format!("{}", CurveError::Type(TypeError::NonPositiveRange)).contains("type error")
);
}
#[test]
fn curve_error_from_type_and_source() {
let te = TypeError::NonPositiveRange;
let ce: CurveError = te.into();
assert!(matches!(ce, CurveError::Type(_)));
let dyn_err: &dyn std::error::Error = &ce;
assert!(dyn_err.source().is_some());
}
#[test]
fn curve_error_no_source_for_plain_variants() {
let ce = CurveError::AnchorNotUnit;
let dyn_err: &dyn std::error::Error = &ce;
assert!(dyn_err.source().is_none());
}
#[test]
fn curve_error_copy_eq() {
let err = CurveError::TooFewNodes { found: 0 };
let copy = err;
assert_eq!(err, copy);
}
#[test]
fn curve_error_debug() {
assert!(format!("{:?}", CurveError::AnchorNotUnit).contains("AnchorNotUnit"));
}
#[test]
fn bootstrap_error_display_all_variants() {
assert!(format!("{}", BootstrapError::InstrumentsNotOrdered { at_index: 2 }).contains('2'));
assert!(format!("{}", BootstrapError::NonIncreasingAnchor { at_index: 5 }).contains('5'));
let m = format!(
"{}",
BootstrapError::LegDidNotConverge {
at_index: 3,
residual: 1.2e-9,
}
);
assert!(m.contains('3'));
assert!(m.contains("converge"));
assert!(format!("{}", BootstrapError::NoBracket { at_index: 7 }).contains('7'));
assert!(
format!(
"{}",
BootstrapError::InvalidInstrument {
at_index: 1,
reason: "negative rate",
}
)
.contains("negative rate")
);
assert!(
format!("{}", BootstrapError::Curve(CurveError::AnchorNotUnit)).contains("curve error")
);
assert!(
format!("{}", BootstrapError::Type(TypeError::NonPositiveRange)).contains("type error")
);
}
#[test]
fn bootstrap_error_from_type() {
let te = TypeError::NonPositiveRange;
let be: BootstrapError = te.into();
assert!(matches!(be, BootstrapError::Type(_)));
let dyn_err: &dyn std::error::Error = &be;
assert!(dyn_err.source().is_some());
}
#[test]
fn bootstrap_error_from_curve() {
let ce = CurveError::AnchorNotUnit;
let be: BootstrapError = ce.into();
assert!(matches!(be, BootstrapError::Curve(_)));
let dyn_err: &dyn std::error::Error = &be;
assert!(dyn_err.source().is_some());
}
#[test]
fn bootstrap_error_no_source_for_plain_variants() {
let be = BootstrapError::NoBracket { at_index: 0 };
let dyn_err: &dyn std::error::Error = &be;
assert!(dyn_err.source().is_none());
}
#[test]
fn bootstrap_error_copy_eq() {
let err = BootstrapError::InstrumentsNotOrdered { at_index: 0 };
let copy = err;
assert_eq!(err, copy);
}
#[test]
fn bootstrap_error_debug() {
assert!(format!("{:?}", BootstrapError::NoBracket { at_index: 0 }).contains("NoBracket"));
}
#[test]
fn bootstrap_error_chained_from_type_through_curve_is_not_automatic() {
let te = TypeError::NonPositiveRange;
let be: BootstrapError = te.into();
assert!(matches!(be, BootstrapError::Type(_)));
}
}