use std::sync::LazyLock;
use crate::errors::QlResult;
use crate::math::integrals::gaussianquadratures::GaussianQuadrature;
use crate::termstructures::credit::defaulttermstructure::DefaultProbabilityTermStructure;
use crate::types::{Probability, Rate, Real, Time};
pub trait HazardRateStructure: DefaultProbabilityTermStructure {
fn hazard_rate_curve_impl(&self, t: Time) -> QlResult<Rate>;
fn survival_probability_from_hazard_rate(&self, t: Time) -> QlResult<Probability> {
static INTEGRAL: LazyLock<QlResult<GaussianQuadrature>> =
LazyLock::new(|| GaussianQuadrature::chebyshev(48));
let integral = INTEGRAL.as_ref().map_err(Clone::clone)?;
let mut sum = 0.0;
for (&weight, &x) in integral
.weights()
.iter()
.zip(integral.abscissas().iter())
.rev()
{
sum += weight * self.hazard_rate_curve_impl((x + 1.0) * t / 2.0)?;
}
Ok((-sum * t / 2.0).exp())
}
fn default_density_from_hazard_rate(&self, t: Time) -> QlResult<Real> {
let hazard_rate = self.hazard_rate_curve_impl(t)?;
Ok(hazard_rate * self.survival_probability_impl(t)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fail;
use crate::patterns::observable::{AsObservable, Observable};
use crate::termstructures::{TermStructure, TermStructureBase};
use crate::time::date::{Date, Month};
use crate::time::daycounters::actual360::Actual360;
use std::cell::Cell;
const INTENSITY: Real = 0.04;
struct DerivedHazardCurve {
base: TermStructureBase,
failing: bool,
}
impl DerivedHazardCurve {
fn new(failing: bool) -> DerivedHazardCurve {
DerivedHazardCurve {
base: TermStructureBase::with_reference_date(
Date::new(15, Month::June, 2026),
None,
Some(Actual360::new()),
),
failing,
}
}
}
impl AsObservable for DerivedHazardCurve {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl TermStructure for DerivedHazardCurve {
fn base(&self) -> &TermStructureBase {
&self.base
}
fn max_date(&self) -> Date {
Date::max_date()
}
}
impl HazardRateStructure for DerivedHazardCurve {
fn hazard_rate_curve_impl(&self, _t: Time) -> QlResult<Rate> {
if self.failing {
fail!("no hazard rate available");
}
Ok(INTENSITY)
}
}
impl DefaultProbabilityTermStructure for DerivedHazardCurve {
fn survival_probability_impl(&self, t: Time) -> QlResult<Probability> {
Ok((-INTENSITY * t).exp())
}
fn default_density_impl(&self, t: Time) -> QlResult<Real> {
self.default_density_from_hazard_rate(t)
}
}
struct QuotedHazardCurve {
inner: DerivedHazardCurve,
}
impl AsObservable for QuotedHazardCurve {
fn observable(&self) -> &Observable {
self.inner.observable()
}
}
impl TermStructure for QuotedHazardCurve {
fn base(&self) -> &TermStructureBase {
self.inner.base()
}
fn max_date(&self) -> Date {
Date::max_date()
}
}
impl HazardRateStructure for QuotedHazardCurve {
fn hazard_rate_curve_impl(&self, t: Time) -> QlResult<Rate> {
self.inner.hazard_rate_curve_impl(t)
}
}
impl DefaultProbabilityTermStructure for QuotedHazardCurve {
fn survival_probability_impl(&self, t: Time) -> QlResult<Probability> {
self.inner.survival_probability_impl(t)
}
fn default_density_impl(&self, t: Time) -> QlResult<Real> {
self.default_density_from_hazard_rate(t)
}
fn hazard_rate_impl(&self, t: Time) -> QlResult<Rate> {
self.hazard_rate_curve_impl(t)
}
}
fn survival(t: Time) -> Probability {
(-INTENSITY * t).exp()
}
#[test]
fn default_density_is_the_hazard_rate_times_the_survival_probability() {
let curve = DerivedHazardCurve::new(false);
for t in [0.0_f64, 0.25, 1.0, 2.5] {
let expected = INTENSITY * survival(t);
assert!((curve.default_density(t, false).unwrap() - expected).abs() < 1.0e-15);
assert!(
(curve.default_density_from_hazard_rate(t).unwrap() - expected).abs() < 1.0e-15
);
}
}
#[test]
fn the_derived_hazard_rate_closes_back_on_the_quoted_one() {
let curve = DerivedHazardCurve::new(false);
for t in [0.25_f64, 1.0, 2.5] {
assert!((curve.hazard_rate(t, false).unwrap() - INTENSITY).abs() < 1.0e-15);
}
}
#[test]
fn overriding_the_base_hazard_rate_agrees_with_the_derived_one() {
let quoted = QuotedHazardCurve {
inner: DerivedHazardCurve::new(false),
};
let derived = DerivedHazardCurve::new(false);
for t in [0.25_f64, 1.0, 2.5] {
assert_eq!(quoted.hazard_rate(t, false).unwrap(), INTENSITY);
assert!(
(quoted.hazard_rate(t, false).unwrap() - derived.hazard_rate(t, false).unwrap())
.abs()
< 1.0e-15
);
assert!(
(quoted.default_density(t, false).unwrap()
- derived.default_density(t, false).unwrap())
.abs()
< 1.0e-15
);
}
}
#[test]
fn the_survival_probability_is_left_to_the_curve() {
let curve = DerivedHazardCurve::new(false);
assert!((curve.survival_probability(2.5, false).unwrap() - survival(2.5)).abs() < 1.0e-15);
assert!(
(curve.default_probability(2.5, false).unwrap() - (1.0 - survival(2.5))).abs()
< 1.0e-15
);
}
#[test]
fn hazard_rate_errors_propagate_through_the_default_density() {
let curve = DerivedHazardCurve::new(true);
let err = curve.default_density(1.0, false).unwrap_err();
assert!(err.message().contains("no hazard rate available"));
let err = curve.hazard_rate(1.0, false).unwrap_err();
assert!(err.message().contains("no hazard rate available"));
assert!((curve.survival_probability(1.0, false).unwrap() - survival(1.0)).abs() < 1.0e-15);
}
struct QuadratureCurve {
inner: DerivedHazardCurve,
hazard: fn(Time) -> QlResult<Rate>,
evaluations: Cell<usize>,
}
impl QuadratureCurve {
fn new(hazard: fn(Time) -> QlResult<Rate>) -> Self {
Self {
inner: DerivedHazardCurve::new(false),
hazard,
evaluations: Cell::new(0),
}
}
}
impl AsObservable for QuadratureCurve {
fn observable(&self) -> &Observable {
self.inner.observable()
}
}
impl TermStructure for QuadratureCurve {
fn base(&self) -> &TermStructureBase {
self.inner.base()
}
fn max_date(&self) -> Date {
Date::max_date()
}
}
impl HazardRateStructure for QuadratureCurve {
fn hazard_rate_curve_impl(&self, t: Time) -> QlResult<Rate> {
self.evaluations.set(self.evaluations.get() + 1);
(self.hazard)(t)
}
}
impl DefaultProbabilityTermStructure for QuadratureCurve {
fn survival_probability_impl(&self, t: Time) -> QlResult<Probability> {
self.survival_probability_from_hazard_rate(t)
}
fn default_density_impl(&self, t: Time) -> QlResult<Real> {
self.default_density_from_hazard_rate(t)
}
}
#[test]
fn quadrature_matches_quantlib_and_analytic_survival() {
let constant = QuadratureCurve::new(|_| Ok(0.04));
let varying = QuadratureCurve::new(|t| Ok(0.04 + 0.01 * t + 0.002 * t * t));
let cases = [
(0.0, 1.0, 1.0),
(0.25, 0.9900480664219722, 0.9897283570410018),
(1.0, 0.9607825787915585, 0.9553525176808173),
(2.5, 0.9048212660113185, 0.867887755838896),
(5.0, 0.8187015234263251, 0.6647038533745758),
];
for (t, constant_ql, varying_ql) in cases {
let flat = constant.survival_probability(t, false).unwrap();
let curved = varying.survival_probability(t, false).unwrap();
assert!((flat - constant_ql).abs() < 1.0e-13);
assert!((curved - varying_ql).abs() < 1.0e-13);
assert!((flat - (-0.04 * t).exp()).abs() < 3.0e-5);
let integrated = 0.04 * t + 0.005 * t * t + 0.002 * t.powi(3) / 3.0;
assert!((curved - (-integrated).exp()).abs() < 6.0e-5);
assert!((constant.hazard_rate(t, false).unwrap() - 0.04).abs() < 1.0e-15);
}
}
#[test]
fn quadrature_maps_all_48_nodes_into_the_time_interval() {
let curve = QuadratureCurve::new(|t| {
assert!((0.0..2.5).contains(&t));
Ok(0.04)
});
curve.survival_probability(2.5, false).unwrap();
assert_eq!(curve.evaluations.get(), 48);
curve.evaluations.set(0);
assert_eq!(curve.survival_probability(0.0, false).unwrap(), 1.0);
assert_eq!(curve.evaluations.get(), 48);
}
#[test]
fn quadrature_propagates_hazard_errors_and_rejects_invalid_public_times() {
let curve = QuadratureCurve::new(|_| fail!("hazard unavailable"));
for t in [0.0, 1.0] {
let err = curve.survival_probability(t, false).unwrap_err();
assert_eq!(err.message(), "hazard unavailable");
}
assert_eq!(curve.evaluations.get(), 2);
for t in [-1.0, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert!(curve.survival_probability(t, true).is_err());
}
assert_eq!(curve.evaluations.get(), 2);
let late_failure = QuadratureCurve::new(|t| {
if t > 0.5 {
fail!("late hazard failure");
}
Ok(0.04)
});
let err = late_failure.survival_probability(1.0, false).unwrap_err();
assert_eq!(err.message(), "late hazard failure");
assert!(late_failure.evaluations.get() > 1);
assert!(late_failure.evaluations.get() < 48);
}
}