use crate::errors::QlResult;
use crate::handle::Handle;
use crate::patterns::observable::Observer;
use crate::quotes::Quote;
use crate::shared::SharedMut;
use crate::termstructures::{TermStructure, TermStructureBase};
use crate::time::date::{Date, Month};
use crate::types::{Probability, Rate, Real, Time};
use crate::{fail, require};
pub struct DefaultProbabilityJumps {
quotes: Vec<Handle<dyn Quote>>,
dates: Vec<Date>,
}
impl DefaultProbabilityJumps {
pub fn new(
base: &TermStructureBase,
quotes: Vec<Handle<dyn Quote>>,
dates: Vec<Date>,
) -> QlResult<Self> {
Self::with_observer(base, &base.updater(), quotes, dates)
}
pub fn with_observer(
base: &TermStructureBase,
observer: &SharedMut<dyn Observer>,
quotes: Vec<Handle<dyn Quote>>,
mut dates: Vec<Date>,
) -> QlResult<Self> {
if dates.is_empty() && !quotes.is_empty() {
let year = base.reference_date()?.year();
require!(
quotes.len() <= (Date::max_date().year() - year + 1) as usize,
"automatic jump dates exceed the maximum supported date"
);
dates = (0..quotes.len())
.map(|i| Date::new(31, Month::December, year + i as i32))
.collect();
}
require!(
quotes.len() == dates.len(),
"mismatch between number of jumps ({}) and jump dates ({})",
quotes.len(),
dates.len()
);
require!(
dates.iter().all(|date| *date != Date::null()),
"null jump date"
);
let jumps = Self { quotes, dates };
jumps.register_observer(observer);
Ok(jumps)
}
pub fn dates(&self) -> &[Date] {
&self.dates
}
pub fn register_observer(&self, observer: &SharedMut<dyn Observer>) {
for quote in &self.quotes {
quote.register_observer(observer);
}
}
fn factor<T: TermStructure + ?Sized>(&self, curve: &T, t: Time) -> QlResult<Probability> {
let mut factor = 1.0;
for (date, handle) in self.dates.iter().zip(&self.quotes) {
let jump_time = curve.time_from_reference(*date)?;
if jump_time > 0.0 && jump_time < t {
let quote = handle.current_link()?;
require!(quote.is_valid(), "invalid jump quote");
let value = quote.value()?;
require!(value > 0.0 && value <= 1.0, "invalid jump value: {value}");
factor *= value;
}
}
Ok(factor)
}
}
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 jumps(&self) -> Option<&DefaultProbabilityJumps> {
None
}
fn jump_dates(&self) -> &[Date] {
self.jumps().map_or(&[], DefaultProbabilityJumps::dates)
}
fn jump_times(&self) -> QlResult<Vec<Time>> {
self.jump_dates()
.iter()
.map(|date| self.time_from_reference(*date))
.collect()
}
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)?;
let factor = match self.jumps() {
Some(jumps) => jumps.factor(self, t)?,
None => 1.0,
};
Ok(factor * 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);
}
}