use crate::errors::QlResult;
use crate::termstructures::TermStructure;
use crate::time::date::Date;
use crate::types::{Probability, Rate, Real, Time};
use crate::{fail, require};
pub trait DefaultProbabilityTermStructure: TermStructure {
fn survival_probability_impl(&self, t: Time) -> QlResult<Probability>;
fn default_density_impl(&self, t: Time) -> QlResult<Real>;
fn as_any(&self) -> Option<&dyn std::any::Any> {
None
}
fn hazard_rate_impl(&self, t: Time) -> QlResult<Rate> {
let survival = self.survival_probability(t, true)?;
if survival == 0.0 {
return Ok(0.0);
}
Ok(self.default_density(t, true)? / survival)
}
fn survival_probability(&self, t: Time, extrapolate: bool) -> QlResult<Probability> {
self.check_range_time(t, extrapolate)?;
self.survival_probability_impl(t)
}
fn survival_probability_date(&self, date: Date, extrapolate: bool) -> QlResult<Probability> {
self.survival_probability(self.time_from_reference(date)?, extrapolate)
}
fn default_probability(&self, t: Time, extrapolate: bool) -> QlResult<Probability> {
Ok(1.0 - self.survival_probability(t, extrapolate)?)
}
fn default_probability_date(&self, date: Date, extrapolate: bool) -> QlResult<Probability> {
Ok(1.0 - self.survival_probability_date(date, extrapolate)?)
}
fn default_probability_between(
&self,
t1: Time,
t2: Time,
extrapolate: bool,
) -> QlResult<Probability> {
if t1.is_nan() || t2.is_nan() || t1 > t2 {
fail!("initial time ({t1}) later than final time ({t2})");
}
let p1 = if t1 < 0.0 {
0.0
} else {
self.default_probability(t1, extrapolate)?
};
let p2 = self.default_probability(t2, extrapolate)?;
Ok(p2 - p1)
}
fn default_probability_between_dates(
&self,
d1: Date,
d2: Date,
extrapolate: bool,
) -> QlResult<Probability> {
require!(d1 <= d2, "initial date ({d1}) later than final date ({d2})");
let p1 = if d1 < self.reference_date()? {
0.0
} else {
self.default_probability_date(d1, extrapolate)?
};
let p2 = self.default_probability_date(d2, extrapolate)?;
Ok(p2 - p1)
}
fn default_density(&self, t: Time, extrapolate: bool) -> QlResult<Real> {
self.check_range_time(t, extrapolate)?;
self.default_density_impl(t)
}
fn default_density_date(&self, date: Date, extrapolate: bool) -> QlResult<Real> {
self.default_density(self.time_from_reference(date)?, extrapolate)
}
fn hazard_rate(&self, t: Time, extrapolate: bool) -> QlResult<Rate> {
self.check_range_time(t, extrapolate)?;
self.hazard_rate_impl(t)
}
fn hazard_rate_date(&self, date: Date, extrapolate: bool) -> QlResult<Rate> {
self.hazard_rate(self.time_from_reference(date)?, extrapolate)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::patterns::observable::{AsObservable, Observable};
use crate::termstructures::TermStructureBase;
use crate::time::date::Month;
use crate::time::daycounters::actual360::Actual360;
const INTENSITY: Real = 0.04;
struct ExponentialCurve {
base: TermStructureBase,
max: Date,
}
impl ExponentialCurve {
fn new(reference: Date) -> ExponentialCurve {
ExponentialCurve {
base: TermStructureBase::with_reference_date(
reference,
None,
Some(Actual360::new()),
),
max: reference + 360,
}
}
}
impl AsObservable for ExponentialCurve {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl TermStructure for ExponentialCurve {
fn base(&self) -> &TermStructureBase {
&self.base
}
fn max_date(&self) -> Date {
self.max
}
}
impl DefaultProbabilityTermStructure for ExponentialCurve {
fn survival_probability_impl(&self, t: Time) -> QlResult<Probability> {
Ok((-INTENSITY * t).exp())
}
fn default_density_impl(&self, t: Time) -> QlResult<Real> {
Ok(INTENSITY * (-INTENSITY * t).exp())
}
}
fn curve() -> ExponentialCurve {
ExponentialCurve::new(Date::new(15, Month::June, 2026))
}
fn survival(t: Time) -> Probability {
(-INTENSITY * t).exp()
}
#[test]
fn survival_probability_checks_range_then_delegates() {
let curve = curve();
assert!((curve.survival_probability(0.5, false).unwrap() - survival(0.5)).abs() < 1.0e-15);
assert_eq!(curve.survival_probability(0.0, false).unwrap(), 1.0);
assert!(curve.survival_probability(-0.5, false).is_err());
assert!(curve.survival_probability(2.0, false).is_err());
assert!((curve.survival_probability(2.0, true).unwrap() - survival(2.0)).abs() < 1.0e-15);
}
#[test]
fn date_variants_convert_through_the_day_counter() {
let curve = curve();
let date = curve.reference_date().unwrap() + 180;
assert!(
(curve.survival_probability_date(date, false).unwrap() - survival(0.5)).abs() < 1.0e-15
);
assert!(
(curve.default_density_date(date, false).unwrap() - INTENSITY * survival(0.5)).abs()
< 1.0e-15
);
assert!((curve.hazard_rate_date(date, false).unwrap() - INTENSITY).abs() < 1.0e-15);
assert!(curve.default_density(-0.5, false).is_err());
assert!(curve.default_density(2.0, false).is_err());
}
#[test]
fn default_probability_is_one_minus_survival() {
let curve = curve();
let date = curve.reference_date().unwrap() + 180;
assert!(
(curve.default_probability(0.5, false).unwrap() - (1.0 - survival(0.5))).abs()
< 1.0e-15
);
assert!(
(curve.default_probability_date(date, false).unwrap() - (1.0 - survival(0.5))).abs()
< 1.0e-15
);
assert_eq!(curve.default_probability(0.0, false).unwrap(), 0.0);
assert!(curve.default_probability(-0.5, false).is_err());
}
#[test]
fn hazard_rate_recovers_the_constant_intensity() {
let curve = curve();
assert!((curve.hazard_rate(0.5, false).unwrap() - INTENSITY).abs() < 1.0e-15);
assert!((curve.hazard_rate(2.0, true).unwrap() - INTENSITY).abs() < 1.0e-15);
assert!(curve.hazard_rate(2.0, false).is_err());
assert!(curve.hazard_rate(-0.5, false).is_err());
}
#[test]
fn default_probability_between_times_clamps_and_orders() {
let curve = curve();
let between = curve
.default_probability_between(0.25, 0.75, false)
.unwrap();
assert!((between - (survival(0.25) - survival(0.75))).abs() < 1.0e-15);
let clamped = curve.default_probability_between(-1.0, 0.5, false).unwrap();
assert!((clamped - (1.0 - survival(0.5))).abs() < 1.0e-15);
assert!(
curve
.default_probability_between(0.75, 0.25, false)
.is_err()
);
}
#[test]
fn default_probability_between_dates_clamps_before_the_reference() {
let curve = curve();
let reference = curve.reference_date().unwrap();
let between = curve
.default_probability_between_dates(reference + 90, reference + 270, false)
.unwrap();
assert!((between - (survival(0.25) - survival(0.75))).abs() < 1.0e-15);
let clamped = curve
.default_probability_between_dates(reference - 1, reference + 180, false)
.unwrap();
assert!((clamped - (1.0 - survival(0.5))).abs() < 1.0e-15);
assert!(
curve
.default_probability_between_dates(reference + 270, reference + 90, false)
.is_err()
);
}
#[test]
fn zero_survival_probability_yields_a_zero_hazard_rate() {
struct DefaultedCurve {
base: TermStructureBase,
}
impl AsObservable for DefaultedCurve {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl TermStructure for DefaultedCurve {
fn base(&self) -> &TermStructureBase {
&self.base
}
fn max_date(&self) -> Date {
Date::max_date()
}
}
impl DefaultProbabilityTermStructure for DefaultedCurve {
fn survival_probability_impl(&self, _t: Time) -> QlResult<Probability> {
Ok(0.0)
}
fn default_density_impl(&self, _t: Time) -> QlResult<Real> {
Ok(1.0)
}
}
let curve = DefaultedCurve {
base: TermStructureBase::with_reference_date(
Date::new(15, Month::June, 2026),
None,
Some(Actual360::new()),
),
};
assert_eq!(curve.hazard_rate(0.5, false).unwrap(), 0.0);
assert_eq!(curve.default_probability(0.5, false).unwrap(), 1.0);
}
}