libitofin/termstructures/credit/
flathazardrate.rs1use crate::errors::QlResult;
30use crate::handle::Handle;
31use crate::patterns::observable::{AsObservable, Observable, Observer, ResetThenNotify};
32use crate::quotes::{Quote, SimpleQuote};
33use crate::settings::Settings;
34use crate::shared::{Shared, SharedMut, shared};
35use crate::termstructures::credit::defaulttermstructure::DefaultProbabilityTermStructure;
36use crate::termstructures::credit::hazardratestructure::HazardRateStructure;
37use crate::termstructures::{TermStructure, TermStructureBase};
38use crate::time::calendar::Calendar;
39use crate::time::date::Date;
40use crate::time::daycounter::DayCounter;
41use crate::types::{Natural, Probability, Rate, Real, Time};
42
43pub struct FlatHazardRate {
45 base: TermStructureBase,
46 hazard_rate: Handle<dyn Quote>,
47 _listener: SharedMut<ResetThenNotify>,
48}
49
50impl FlatHazardRate {
51 fn assemble(base: TermStructureBase, hazard_rate: Handle<dyn Quote>) -> FlatHazardRate {
52 let listener = ResetThenNotify::delivering(base.updater(), || {});
53 hazard_rate.register_observer(&(listener.clone() as SharedMut<dyn Observer>));
54 FlatHazardRate {
55 base,
56 hazard_rate,
57 _listener: listener,
58 }
59 }
60
61 fn wrap(value: Rate) -> Handle<dyn Quote> {
62 Handle::new(shared(SimpleQuote::new(value)) as Shared<dyn Quote>)
63 }
64
65 pub fn new(
67 reference_date: Date,
68 hazard_rate: Handle<dyn Quote>,
69 day_counter: DayCounter,
70 ) -> FlatHazardRate {
71 let base = TermStructureBase::with_reference_date(reference_date, None, Some(day_counter));
72 Self::assemble(base, hazard_rate)
73 }
74
75 pub fn with_rate(
77 reference_date: Date,
78 hazard_rate: Rate,
79 day_counter: DayCounter,
80 ) -> FlatHazardRate {
81 Self::new(reference_date, Self::wrap(hazard_rate), day_counter)
82 }
83
84 pub fn moving(
86 settlement_days: Natural,
87 calendar: Calendar,
88 hazard_rate: Handle<dyn Quote>,
89 day_counter: DayCounter,
90 settings: Shared<Settings<Date>>,
91 ) -> FlatHazardRate {
92 let base =
93 TermStructureBase::moving(settlement_days, calendar, Some(day_counter), settings);
94 Self::assemble(base, hazard_rate)
95 }
96
97 pub fn moving_with_rate(
99 settlement_days: Natural,
100 calendar: Calendar,
101 hazard_rate: Rate,
102 day_counter: DayCounter,
103 settings: Shared<Settings<Date>>,
104 ) -> FlatHazardRate {
105 Self::moving(
106 settlement_days,
107 calendar,
108 Self::wrap(hazard_rate),
109 day_counter,
110 settings,
111 )
112 }
113
114 fn hazard_rate_value(&self) -> QlResult<Rate> {
115 self.hazard_rate.current_link()?.value()
116 }
117}
118
119impl AsObservable for FlatHazardRate {
120 fn observable(&self) -> &Observable {
121 self.base.observable()
122 }
123}
124
125impl TermStructure for FlatHazardRate {
126 fn base(&self) -> &TermStructureBase {
127 &self.base
128 }
129
130 fn max_date(&self) -> Date {
131 Date::max_date()
132 }
133}
134
135impl HazardRateStructure for FlatHazardRate {
136 fn hazard_rate_curve_impl(&self, _t: Time) -> QlResult<Rate> {
137 self.hazard_rate_value()
138 }
139}
140
141impl DefaultProbabilityTermStructure for FlatHazardRate {
142 fn survival_probability_impl(&self, t: Time) -> QlResult<Probability> {
143 Ok((-self.hazard_rate_value()? * t).exp())
144 }
145
146 fn default_density_impl(&self, t: Time) -> QlResult<Real> {
147 self.default_density_from_hazard_rate(t)
148 }
149
150 fn hazard_rate_impl(&self, t: Time) -> QlResult<Rate> {
151 self.hazard_rate_curve_impl(t)
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use crate::test_support::{Flag, as_observer};
159 use crate::time::businessdayconvention::BusinessDayConvention;
160 use crate::time::calendars::target::Target;
161 use crate::time::date::Month;
162 use crate::time::daycounters::actual360::Actual360;
163 use crate::time::timeunit::TimeUnit;
164
165 const HAZARD_RATE: Rate = 0.0100;
166 const TOLERANCE: Real = 1.0e-10;
167 const N: usize = 20;
168
169 fn today() -> Date {
170 Date::new(15, Month::June, 2026)
171 }
172
173 fn handle(quote: &Shared<SimpleQuote>) -> Handle<dyn Quote> {
174 Handle::new(quote.clone() as Shared<dyn Quote>)
175 }
176
177 fn one_year_on(calendar: &Calendar, d: Date) -> Date {
180 calendar.advance(
181 d,
182 1,
183 TimeUnit::Years,
184 BusinessDayConvention::Following,
185 false,
186 )
187 }
188
189 #[test]
196 fn flat_hazard_rate_reproduces_the_closed_form_default_probability() {
197 let quote = shared(SimpleQuote::new(HAZARD_RATE));
198 let day_counter = Actual360::new();
199 let calendar = Target::new();
200 let start_date = today();
201 let curve = FlatHazardRate::new(today(), handle("e), day_counter.clone());
202
203 let mut end_date = start_date;
204 for _ in 0..N {
205 end_date = one_year_on(&calendar, end_date);
206 let t = day_counter.year_fraction(start_date, end_date);
207 let probability = 1.0 - (-HAZARD_RATE * t).exp();
208 let computed = curve.default_probability(t, false).unwrap();
209 assert!(
210 (probability - computed).abs() <= TOLERANCE,
211 "failed to reproduce probability for flat hazard rate at t = {t}: \
212 calculated {computed}, expected {probability}"
213 );
214 }
215 }
216
217 #[test]
221 fn default_probabilities_are_self_consistent_across_dates_and_times() {
222 let quote = shared(SimpleQuote::new(HAZARD_RATE));
223 let day_counter = Actual360::new();
224 let calendar = Target::new();
225 let curve = FlatHazardRate::new(today(), handle("e), day_counter.clone());
226
227 let mut end_date = today();
228 for _ in 0..N {
229 let start_date = end_date;
230 end_date = one_year_on(&calendar, end_date);
231
232 let p_start = curve.default_probability_date(start_date, false).unwrap();
233 let p_end = curve.default_probability_date(end_date, false).unwrap();
234 let p_between_computed = curve
235 .default_probability_between_dates(start_date, end_date, false)
236 .unwrap();
237 let p_between = p_end - p_start;
238 assert!(
239 (p_between - p_between_computed).abs() <= TOLERANCE,
240 "failed to reproduce probability(d1, d2): \
241 calculated {p_between_computed}, expected {p_between}"
242 );
243
244 let t2 = day_counter.year_fraction(today(), end_date);
245 let time_probability = curve.default_probability(t2, false).unwrap();
246 assert!(
247 (time_probability - p_end).abs() <= TOLERANCE,
248 "single-time probability {time_probability} and single-date \
249 probability {p_end} do not match"
250 );
251
252 let t1 = day_counter.year_fraction(today(), start_date);
253 let time_probability = curve.default_probability_between(t1, t2, false).unwrap();
254 assert!(
255 (time_probability - p_between_computed).abs() <= TOLERANCE,
256 "double-time probability {time_probability} and double-date \
257 probability {p_between_computed} do not match"
258 );
259 }
260 }
261
262 #[test]
266 fn density_and_hazard_rate_match_their_closed_forms() {
267 let curve = FlatHazardRate::with_rate(today(), HAZARD_RATE, Actual360::new());
268 for t in [0.0_f64, 0.5, 1.0, 5.0, 20.0] {
269 let survival = (-HAZARD_RATE * t).exp();
270 assert!((curve.survival_probability(t, false).unwrap() - survival).abs() <= TOLERANCE);
271 assert!(
272 (curve.default_density(t, false).unwrap() - HAZARD_RATE * survival).abs()
273 <= TOLERANCE
274 );
275 assert!((curve.hazard_rate(t, false).unwrap() - HAZARD_RATE).abs() <= TOLERANCE);
276 }
277 }
278
279 #[test]
280 fn quote_change_notifies_observers_and_refreshes_the_curve() {
281 let quote = shared(SimpleQuote::new(HAZARD_RATE));
282 let curve = FlatHazardRate::new(today(), handle("e), Actual360::new());
283 assert!(
284 (curve.survival_probability(2.0, false).unwrap() - (-0.02_f64).exp()).abs()
285 <= TOLERANCE
286 );
287
288 let flag = Flag::new();
289 curve.observable().register_observer(&as_observer(&flag));
290 quote.set_value(0.0200);
291
292 assert!(
293 Flag::is_up(&flag),
294 "quote change must reach curve observers"
295 );
296 assert!(
297 (curve.survival_probability(2.0, false).unwrap() - (-0.04_f64).exp()).abs()
298 <= TOLERANCE
299 );
300 assert!((curve.hazard_rate(1.0, false).unwrap() - 0.0200).abs() <= TOLERANCE);
301 }
302
303 #[test]
304 fn moving_curve_follows_the_evaluation_date() {
305 let settings = shared(Settings::new());
306 settings.set_evaluation_date(Date::new(15, Month::January, 2026));
307 let curve = FlatHazardRate::moving_with_rate(
308 2,
309 Target::new(),
310 HAZARD_RATE,
311 Actual360::new(),
312 settings.clone(),
313 );
314 assert_eq!(
315 curve.reference_date().unwrap(),
316 Date::new(19, Month::January, 2026)
317 );
318
319 let flag = Flag::new();
320 curve.observable().register_observer(&as_observer(&flag));
321 settings.set_evaluation_date(Date::new(16, Month::January, 2026));
322
323 assert!(Flag::is_up(&flag));
324 assert_eq!(
325 curve.reference_date().unwrap(),
326 Date::new(20, Month::January, 2026)
327 );
328 let survival = curve
329 .survival_probability_date(Date::new(20, Month::January, 2027), false)
330 .unwrap();
331 assert!((survival - (-HAZARD_RATE * 365.0 / 360.0).exp()).abs() <= TOLERANCE);
332 }
333
334 #[test]
335 fn empty_or_unset_quotes_error_instead_of_pricing() {
336 let curve = FlatHazardRate::new(today(), Handle::empty(), Actual360::new());
337 assert!(curve.survival_probability(1.0, false).is_err());
338 assert!(curve.hazard_rate(1.0, false).is_err());
339
340 let unset = shared(SimpleQuote::default());
341 let curve = FlatHazardRate::new(
342 today(),
343 Handle::new(unset as Shared<dyn Quote>),
344 Actual360::new(),
345 );
346 assert!(curve.default_density(1.0, false).is_err());
347 }
348}