use crate::errors::QlResult;
use crate::handle::Handle;
use crate::patterns::observable::{AsObservable, Observable, Observer, ResetThenNotify};
use crate::quotes::{Quote, SimpleQuote};
use crate::settings::Settings;
use crate::shared::{Shared, SharedMut, shared};
use crate::termstructures::credit::defaulttermstructure::DefaultProbabilityTermStructure;
use crate::termstructures::credit::hazardratestructure::HazardRateStructure;
use crate::termstructures::{TermStructure, TermStructureBase};
use crate::time::calendar::Calendar;
use crate::time::date::Date;
use crate::time::daycounter::DayCounter;
use crate::types::{Natural, Probability, Rate, Real, Time};
pub struct FlatHazardRate {
base: TermStructureBase,
hazard_rate: Handle<dyn Quote>,
_listener: SharedMut<ResetThenNotify>,
}
impl FlatHazardRate {
fn assemble(base: TermStructureBase, hazard_rate: Handle<dyn Quote>) -> FlatHazardRate {
let listener = ResetThenNotify::delivering(base.updater(), || {});
hazard_rate.register_observer(&(listener.clone() as SharedMut<dyn Observer>));
FlatHazardRate {
base,
hazard_rate,
_listener: listener,
}
}
fn wrap(value: Rate) -> Handle<dyn Quote> {
Handle::new(shared(SimpleQuote::new(value)) as Shared<dyn Quote>)
}
pub fn new(
reference_date: Date,
hazard_rate: Handle<dyn Quote>,
day_counter: DayCounter,
) -> FlatHazardRate {
let base = TermStructureBase::with_reference_date(reference_date, None, Some(day_counter));
Self::assemble(base, hazard_rate)
}
pub fn with_rate(
reference_date: Date,
hazard_rate: Rate,
day_counter: DayCounter,
) -> FlatHazardRate {
Self::new(reference_date, Self::wrap(hazard_rate), day_counter)
}
pub fn moving(
settlement_days: Natural,
calendar: Calendar,
hazard_rate: Handle<dyn Quote>,
day_counter: DayCounter,
settings: Shared<Settings<Date>>,
) -> FlatHazardRate {
let base =
TermStructureBase::moving(settlement_days, calendar, Some(day_counter), settings);
Self::assemble(base, hazard_rate)
}
pub fn moving_with_rate(
settlement_days: Natural,
calendar: Calendar,
hazard_rate: Rate,
day_counter: DayCounter,
settings: Shared<Settings<Date>>,
) -> FlatHazardRate {
Self::moving(
settlement_days,
calendar,
Self::wrap(hazard_rate),
day_counter,
settings,
)
}
fn hazard_rate_value(&self) -> QlResult<Rate> {
self.hazard_rate.current_link()?.value()
}
}
impl AsObservable for FlatHazardRate {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl TermStructure for FlatHazardRate {
fn base(&self) -> &TermStructureBase {
&self.base
}
fn max_date(&self) -> Date {
Date::max_date()
}
}
impl HazardRateStructure for FlatHazardRate {
fn hazard_rate_curve_impl(&self, _t: Time) -> QlResult<Rate> {
self.hazard_rate_value()
}
}
impl DefaultProbabilityTermStructure for FlatHazardRate {
fn as_any(&self) -> Option<&dyn std::any::Any> {
Some(self)
}
fn survival_probability_impl(&self, t: Time) -> QlResult<Probability> {
Ok((-self.hazard_rate_value()? * t).exp())
}
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)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::{Flag, as_observer};
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendars::target::Target;
use crate::time::date::Month;
use crate::time::daycounters::actual360::Actual360;
use crate::time::timeunit::TimeUnit;
const HAZARD_RATE: Rate = 0.0100;
const TOLERANCE: Real = 1.0e-10;
const N: usize = 20;
fn today() -> Date {
Date::new(15, Month::June, 2026)
}
fn handle(quote: &Shared<SimpleQuote>) -> Handle<dyn Quote> {
Handle::new(quote.clone() as Shared<dyn Quote>)
}
fn one_year_on(calendar: &Calendar, d: Date) -> Date {
calendar.advance(
d,
1,
TimeUnit::Years,
BusinessDayConvention::Following,
false,
)
}
#[test]
fn flat_hazard_rate_reproduces_the_closed_form_default_probability() {
let quote = shared(SimpleQuote::new(HAZARD_RATE));
let day_counter = Actual360::new();
let calendar = Target::new();
let start_date = today();
let curve = FlatHazardRate::new(today(), handle("e), day_counter.clone());
let mut end_date = start_date;
for _ in 0..N {
end_date = one_year_on(&calendar, end_date);
let t = day_counter.year_fraction(start_date, end_date);
let probability = 1.0 - (-HAZARD_RATE * t).exp();
let computed = curve.default_probability(t, false).unwrap();
assert!(
(probability - computed).abs() <= TOLERANCE,
"failed to reproduce probability for flat hazard rate at t = {t}: \
calculated {computed}, expected {probability}"
);
}
}
#[test]
fn default_probabilities_are_self_consistent_across_dates_and_times() {
let quote = shared(SimpleQuote::new(HAZARD_RATE));
let day_counter = Actual360::new();
let calendar = Target::new();
let curve = FlatHazardRate::new(today(), handle("e), day_counter.clone());
let mut end_date = today();
for _ in 0..N {
let start_date = end_date;
end_date = one_year_on(&calendar, end_date);
let p_start = curve.default_probability_date(start_date, false).unwrap();
let p_end = curve.default_probability_date(end_date, false).unwrap();
let p_between_computed = curve
.default_probability_between_dates(start_date, end_date, false)
.unwrap();
let p_between = p_end - p_start;
assert!(
(p_between - p_between_computed).abs() <= TOLERANCE,
"failed to reproduce probability(d1, d2): \
calculated {p_between_computed}, expected {p_between}"
);
let t2 = day_counter.year_fraction(today(), end_date);
let time_probability = curve.default_probability(t2, false).unwrap();
assert!(
(time_probability - p_end).abs() <= TOLERANCE,
"single-time probability {time_probability} and single-date \
probability {p_end} do not match"
);
let t1 = day_counter.year_fraction(today(), start_date);
let time_probability = curve.default_probability_between(t1, t2, false).unwrap();
assert!(
(time_probability - p_between_computed).abs() <= TOLERANCE,
"double-time probability {time_probability} and double-date \
probability {p_between_computed} do not match"
);
}
}
#[test]
fn density_and_hazard_rate_match_their_closed_forms() {
let curve = FlatHazardRate::with_rate(today(), HAZARD_RATE, Actual360::new());
for t in [0.0_f64, 0.5, 1.0, 5.0, 20.0] {
let survival = (-HAZARD_RATE * t).exp();
assert!((curve.survival_probability(t, false).unwrap() - survival).abs() <= TOLERANCE);
assert!(
(curve.default_density(t, false).unwrap() - HAZARD_RATE * survival).abs()
<= TOLERANCE
);
assert!((curve.hazard_rate(t, false).unwrap() - HAZARD_RATE).abs() <= TOLERANCE);
}
}
#[test]
fn quote_change_notifies_observers_and_refreshes_the_curve() {
let quote = shared(SimpleQuote::new(HAZARD_RATE));
let curve = FlatHazardRate::new(today(), handle("e), Actual360::new());
assert!(
(curve.survival_probability(2.0, false).unwrap() - (-0.02_f64).exp()).abs()
<= TOLERANCE
);
let flag = Flag::new();
curve.observable().register_observer(&as_observer(&flag));
quote.set_value(0.0200);
assert!(
Flag::is_up(&flag),
"quote change must reach curve observers"
);
assert!(
(curve.survival_probability(2.0, false).unwrap() - (-0.04_f64).exp()).abs()
<= TOLERANCE
);
assert!((curve.hazard_rate(1.0, false).unwrap() - 0.0200).abs() <= TOLERANCE);
}
#[test]
fn moving_curve_follows_the_evaluation_date() {
let settings = shared(Settings::new());
settings.set_evaluation_date(Date::new(15, Month::January, 2026));
let curve = FlatHazardRate::moving_with_rate(
2,
Target::new(),
HAZARD_RATE,
Actual360::new(),
settings.clone(),
);
assert_eq!(
curve.reference_date().unwrap(),
Date::new(19, Month::January, 2026)
);
let flag = Flag::new();
curve.observable().register_observer(&as_observer(&flag));
settings.set_evaluation_date(Date::new(16, Month::January, 2026));
assert!(Flag::is_up(&flag));
assert_eq!(
curve.reference_date().unwrap(),
Date::new(20, Month::January, 2026)
);
let survival = curve
.survival_probability_date(Date::new(20, Month::January, 2027), false)
.unwrap();
assert!((survival - (-HAZARD_RATE * 365.0 / 360.0).exp()).abs() <= TOLERANCE);
}
#[test]
fn empty_or_unset_quotes_error_instead_of_pricing() {
let curve = FlatHazardRate::new(today(), Handle::empty(), Actual360::new());
assert!(curve.survival_probability(1.0, false).is_err());
assert!(curve.hazard_rate(1.0, false).is_err());
let unset = shared(SimpleQuote::default());
let curve = FlatHazardRate::new(
today(),
Handle::new(unset as Shared<dyn Quote>),
Actual360::new(),
);
assert!(curve.default_density(1.0, false).is_err());
}
}