1use std::cell::{Cell, RefCell};
26use std::rc::Weak;
27
28use crate::errors::{QlError, QlResult};
29use crate::handle::{Handle, RelinkableHandle};
30use crate::instrument::Instrument;
31use crate::instruments::{CdsTerms, CreditDefaultSwap, ProtectionSide};
32use crate::patterns::observable::{AsObservable, Observable};
33use crate::pricingengine::PricingEngine;
34use crate::pricingengines::credit::MidPointCdsEngine;
35use crate::quotes::Quote;
36use crate::require;
37use crate::settings::Settings;
38use crate::shared::{Shared, SharedMut, shared_mut};
39use crate::termstructures::bootstraphelper::{BootstrapHelperBase, BootstrapHelperShared};
40use crate::termstructures::credit::defaulttermstructure::DefaultProbabilityTermStructure;
41use crate::termstructures::yieldtermstructure::YieldTermStructure;
42use crate::time::businessdayconvention::BusinessDayConvention;
43use crate::time::calendar::Calendar;
44use crate::time::date::Date;
45use crate::time::dategenerationrule::DateGeneration;
46use crate::time::daycounter::DayCounter;
47use crate::time::frequency::Frequency;
48use crate::time::period::Period;
49use crate::time::schedule::{MakeSchedule, Schedule};
50use crate::types::{Integer, Real};
51
52pub type DefaultProbabilityHelperBase = BootstrapHelperBase<dyn DefaultProbabilityTermStructure>;
55
56pub trait DefaultProbabilityHelper: AsObservable {
67 fn base(&self) -> &DefaultProbabilityHelperBase;
69
70 fn implied_quote(&self) -> QlResult<Real>;
75
76 fn quote(&self) -> &Handle<dyn Quote> {
78 self.base().quote()
79 }
80
81 fn quote_error(&self) -> QlResult<Real> {
83 Ok(self.base().quote_value()? - self.implied_quote()?)
84 }
85
86 fn set_term_structure(&self, term_structure: &Shared<dyn DefaultProbabilityTermStructure>) {
91 self.base().set_term_structure(term_structure);
92 }
93
94 fn earliest_date(&self) -> Date {
96 self.base().earliest_date()
97 }
98
99 fn maturity_date(&self) -> Date {
101 self.base().maturity_date()
102 }
103
104 fn latest_relevant_date(&self) -> Date {
106 self.base().latest_relevant_date()
107 }
108
109 fn pillar_date(&self) -> Date {
111 self.base().pillar_date()
112 }
113
114 fn latest_date(&self) -> Date {
116 self.base().latest_date()
117 }
118}
119
120pub trait RelativeDateDefaultProbabilityHelper: DefaultProbabilityHelper {
128 fn initialize_dates(&self);
130}
131
132impl BootstrapHelperShared for dyn DefaultProbabilityHelper {
136 type TS = dyn DefaultProbabilityTermStructure;
137
138 fn set_term_structure(&self, term_structure: &Shared<dyn DefaultProbabilityTermStructure>) {
139 DefaultProbabilityHelper::set_term_structure(self, term_structure);
140 }
141
142 fn quote_value(&self) -> QlResult<Real> {
143 self.base().quote_value()
144 }
145
146 fn quote_error(&self) -> QlResult<Real> {
147 DefaultProbabilityHelper::quote_error(self)
148 }
149
150 fn pillar_date(&self) -> Date {
151 DefaultProbabilityHelper::pillar_date(self)
152 }
153
154 fn latest_relevant_date(&self) -> Date {
155 DefaultProbabilityHelper::latest_relevant_date(self)
156 }
157
158 fn maturity_date(&self) -> Date {
159 DefaultProbabilityHelper::maturity_date(self)
160 }
161}
162
163pub struct CdsHelperTerms {
172 pub settles_accrual: bool,
174 pub pays_at_default_time: bool,
177 pub start_date: Option<Date>,
180 pub last_period_day_counter: Option<DayCounter>,
182 pub rebates_accrual: bool,
184}
185
186impl Default for CdsHelperTerms {
187 fn default() -> CdsHelperTerms {
188 CdsHelperTerms {
189 settles_accrual: true,
190 pays_at_default_time: true,
191 start_date: None,
192 last_period_day_counter: None,
193 rebates_accrual: true,
194 }
195 }
196}
197
198pub struct SpreadCdsHelper {
215 base: DefaultProbabilityHelperBase,
216 tenor: Period,
217 settlement_days: Integer,
218 calendar: Calendar,
219 frequency: Frequency,
220 payment_convention: BusinessDayConvention,
221 rule: DateGeneration,
222 day_counter: DayCounter,
223 recovery_rate: Real,
224 discount_curve: Handle<dyn YieldTermStructure>,
225 settles_accrual: bool,
226 pays_at_default_time: bool,
227 start_date: Option<Date>,
228 last_period_day_counter: Option<DayCounter>,
229 rebates_accrual: bool,
230 settings: Shared<Settings<Date>>,
231 schedule: RefCell<Schedule>,
232 protection_start: Cell<Date>,
233 probability: RelinkableHandle<dyn DefaultProbabilityTermStructure>,
234 swap: RefCell<QlResult<CreditDefaultSwap>>,
235}
236
237fn engine_not_reset() -> QlError {
240 QlError::new(
241 "the helper's credit default swap is built when the bootstrapping curve is set",
242 file!(),
243 line!(),
244 )
245}
246
247impl SpreadCdsHelper {
248 #[allow(clippy::too_many_arguments)]
258 pub fn new(
259 running_spread: Handle<dyn Quote>,
260 tenor: Period,
261 settlement_days: Integer,
262 calendar: Calendar,
263 frequency: Frequency,
264 payment_convention: BusinessDayConvention,
265 rule: DateGeneration,
266 day_counter: DayCounter,
267 recovery_rate: Real,
268 discount_curve: Handle<dyn YieldTermStructure>,
269 settings: Shared<Settings<Date>>,
270 ) -> QlResult<Shared<SpreadCdsHelper>> {
271 SpreadCdsHelper::with_terms(
272 running_spread,
273 tenor,
274 settlement_days,
275 calendar,
276 frequency,
277 payment_convention,
278 rule,
279 day_counter,
280 recovery_rate,
281 discount_curve,
282 CdsHelperTerms::default(),
283 settings,
284 )
285 }
286
287 #[allow(clippy::too_many_arguments)]
300 pub fn with_terms(
301 running_spread: Handle<dyn Quote>,
302 tenor: Period,
303 settlement_days: Integer,
304 calendar: Calendar,
305 frequency: Frequency,
306 payment_convention: BusinessDayConvention,
307 rule: DateGeneration,
308 day_counter: DayCounter,
309 recovery_rate: Real,
310 discount_curve: Handle<dyn YieldTermStructure>,
311 terms: CdsHelperTerms,
312 settings: Shared<Settings<Date>>,
313 ) -> QlResult<Shared<SpreadCdsHelper>> {
314 require!(
315 !matches!(
316 rule,
317 DateGeneration::CDS | DateGeneration::CDS2015 | DateGeneration::OldCDS
318 ),
319 "the post-Big-Bang date-generation rules need cdsMaturity, which is not ported yet \
320 (defaultprobabilityhelpers.cpp:85-88)"
321 );
322 Ok(Shared::new_cyclic(|weak: &Weak<SpreadCdsHelper>| {
323 let weak = weak.clone();
324 let on_eval_change = Box::new(move || {
325 if let Some(helper) = weak.upgrade() {
326 helper.initialize_dates();
327 helper.reset_engine();
328 }
329 });
330 let base = BootstrapHelperBase::new_relative(
331 running_spread,
332 Shared::clone(&settings),
333 true,
334 on_eval_change,
335 );
336 discount_curve.register_observer(&base.observer());
337 let helper = SpreadCdsHelper {
338 base,
339 tenor,
340 settlement_days,
341 calendar,
342 frequency,
343 payment_convention,
344 rule,
345 day_counter,
346 recovery_rate,
347 discount_curve,
348 settles_accrual: terms.settles_accrual,
349 pays_at_default_time: terms.pays_at_default_time,
350 start_date: terms.start_date,
351 last_period_day_counter: terms.last_period_day_counter,
352 rebates_accrual: terms.rebates_accrual,
353 settings,
354 schedule: RefCell::new(Schedule::from_dates(Vec::new())),
355 protection_start: Cell::new(Date::null()),
356 probability: RelinkableHandle::empty(),
357 swap: RefCell::new(Err(engine_not_reset())),
358 };
359 helper.initialize_dates();
360 helper
361 }))
362 }
363
364 pub fn protection_start(&self) -> Date {
367 self.protection_start.get()
368 }
369
370 fn reset_engine(&self) {
390 *self.swap.borrow_mut() = self.build_swap();
391 }
392
393 fn build_swap(&self) -> QlResult<CreditDefaultSwap> {
406 let mut swap = CreditDefaultSwap::with_terms(
407 ProtectionSide::Buyer,
408 100.0,
409 0.01,
410 self.schedule.borrow().clone(),
411 self.payment_convention,
412 self.day_counter.clone(),
413 CdsTerms {
414 settles_accrual: self.settles_accrual,
415 pays_at_default_time: self.pays_at_default_time,
416 protection_start: Some(self.protection_start.get()),
417 last_period_day_counter: self.last_period_day_counter.clone(),
418 rebates_accrual: self.rebates_accrual,
419 ..CdsTerms::default()
420 },
421 Shared::clone(&self.settings),
422 )?;
423 let engine = MidPointCdsEngine::new(
424 self.probability.handle(),
425 self.recovery_rate,
426 self.discount_curve.clone(),
427 None,
428 Shared::clone(&self.settings),
429 );
430 swap.base_mut()
431 .set_pricing_engine(shared_mut(engine) as SharedMut<dyn PricingEngine>);
432 Ok(swap)
433 }
434}
435
436impl AsObservable for SpreadCdsHelper {
437 fn observable(&self) -> &Observable {
438 self.base.observable()
439 }
440}
441
442impl DefaultProbabilityHelper for SpreadCdsHelper {
443 fn base(&self) -> &DefaultProbabilityHelperBase {
444 &self.base
445 }
446
447 fn implied_quote(&self) -> QlResult<Real> {
454 let mut swap = self.swap.borrow_mut();
455 let swap = swap.as_mut().map_err(|error| error.clone())?;
456 swap.recalculate()?;
457 swap.fair_spread()
458 }
459
460 fn set_term_structure(&self, term_structure: &Shared<dyn DefaultProbabilityTermStructure>) {
468 self.base.set_term_structure(term_structure);
469 self.probability
470 .link_to_weak(Shared::downgrade(term_structure));
471 self.reset_engine();
472 }
473}
474
475impl RelativeDateDefaultProbabilityHelper for SpreadCdsHelper {
476 fn initialize_dates(&self) {
489 let evaluation_date = self
490 .base
491 .evaluation_date()
492 .expect("a relative-date helper always tracks an evaluation date");
493 let protection_start = evaluation_date + self.settlement_days;
494 self.protection_start.set(protection_start);
495
496 let mut start_date = self.start_date.unwrap_or(protection_start);
497 if self.rule != DateGeneration::CDS && self.rule != DateGeneration::CDS2015 {
498 start_date = self.calendar.adjust(start_date, self.payment_convention);
499 }
500 let reference_date = match self.start_date {
501 Some(date) => date + self.settlement_days,
502 None => protection_start,
503 };
504 let end_date = reference_date + self.tenor;
505
506 let schedule = MakeSchedule::new()
507 .from(start_date)
508 .to(end_date)
509 .with_frequency(self.frequency)
510 .with_calendar(self.calendar.clone())
511 .with_convention(self.payment_convention)
512 .with_termination_date_convention(BusinessDayConvention::Unadjusted)
513 .with_rule(self.rule)
514 .build();
515
516 self.base.set_earliest_date(schedule.date(0));
517 self.base.set_latest_date(
518 self.calendar
519 .adjust(schedule.date(schedule.len() - 1), self.payment_convention),
520 );
521 *self.schedule.borrow_mut() = schedule;
522 }
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528
529 #[test]
537 fn credit_helpers_satisfy_the_driver_bound() {
538 fn accepts_driver_helper<H>()
539 where
540 H: BootstrapHelperShared<TS = dyn DefaultProbabilityTermStructure> + ?Sized,
541 {
542 }
543 accepts_driver_helper::<dyn DefaultProbabilityHelper>();
544 }
545
546 use crate::interestrate::Compounding;
547 use crate::quotes::SimpleQuote;
548 use crate::shared::shared;
549 use crate::termstructures::credit::flathazardrate::FlatHazardRate;
550 use crate::termstructures::yields::FlatForward;
551 use crate::test_support::{Flag, as_observer};
552 use crate::time::calendars::target::Target;
553 use crate::time::date::Month;
554 use crate::time::daycounters::actual360::Actual360;
555 use crate::time::daycounters::actual365fixed::Actual365Fixed;
556 use crate::time::timeunit::TimeUnit;
557
558 fn today() -> Date {
561 Date::new(15, Month::June, 2026)
562 }
563
564 fn five_years() -> Period {
565 Period::new(5, TimeUnit::Years)
566 }
567
568 fn settings_at(evaluation_date: Date) -> Shared<Settings<Date>> {
569 let settings = shared(Settings::new());
570 settings.set_evaluation_date(evaluation_date);
571 settings
572 }
573
574 fn discount(settlement: Date) -> Handle<dyn YieldTermStructure> {
575 Handle::new(shared(FlatForward::with_rate(
576 settlement,
577 0.03,
578 Actual365Fixed::new(),
579 Compounding::Continuous,
580 Frequency::Annual,
581 )) as Shared<dyn YieldTermStructure>)
582 }
583
584 fn helper(settings: &Shared<Settings<Date>>, terms: CdsHelperTerms) -> Shared<SpreadCdsHelper> {
587 SpreadCdsHelper::with_terms(
588 Handle::new(shared(SimpleQuote::new(0.01)) as Shared<dyn Quote>),
589 five_years(),
590 1,
591 Target::new(),
592 Frequency::Quarterly,
593 BusinessDayConvention::Following,
594 DateGeneration::TwentiethIMM,
595 Actual360::new(),
596 0.4,
597 discount(today()),
598 terms,
599 Shared::clone(settings),
600 )
601 .unwrap()
602 }
603
604 fn expected_schedule(start_date: Date, end_date: Date) -> Schedule {
607 MakeSchedule::new()
608 .from(Target::new().adjust(start_date, BusinessDayConvention::Following))
609 .to(end_date)
610 .with_frequency(Frequency::Quarterly)
611 .with_calendar(Target::new())
612 .with_convention(BusinessDayConvention::Following)
613 .with_termination_date_convention(BusinessDayConvention::Unadjusted)
614 .with_rule(DateGeneration::TwentiethIMM)
615 .build()
616 }
617
618 fn last_date(schedule: &Schedule) -> Date {
619 schedule.date(schedule.len() - 1)
620 }
621
622 #[test]
628 fn initialize_dates_spans_protection_start_to_the_tenor() {
629 let settings = settings_at(today());
630 let helper = helper(&settings, CdsHelperTerms::default());
631 let calendar = Target::new();
632
633 let protection_start = today() + 1;
634 assert_eq!(helper.protection_start(), protection_start);
635
636 let schedule = expected_schedule(protection_start, protection_start + five_years());
637 assert_eq!(
638 helper.earliest_date(),
639 calendar.adjust(protection_start, BusinessDayConvention::Following)
640 );
641 assert_eq!(helper.earliest_date(), schedule.date(0));
642 assert_eq!(
643 helper.latest_date(),
644 calendar.adjust(last_date(&schedule), BusinessDayConvention::Following)
645 );
646 }
647
648 #[test]
653 fn the_node_sits_on_the_rolled_maturity() {
654 let settings = settings_at(today());
655 let helper = helper(&settings, CdsHelperTerms::default());
656
657 let schedule = expected_schedule(today() + 1, today() + 1 + five_years());
658 let rolled = Target::new().adjust(last_date(&schedule), BusinessDayConvention::Following);
659 assert_eq!(helper.latest_date(), rolled);
660 assert_eq!(helper.pillar_date(), rolled);
661 assert_eq!(helper.latest_relevant_date(), rolled);
662 assert_eq!(helper.maturity_date(), rolled);
663 }
664
665 #[test]
670 fn an_explicit_start_date_offsets_the_maturity_by_the_settlement_days() {
671 let settings = settings_at(today());
672 let start_date = Date::new(20, Month::March, 2026);
673 let helper = helper(
674 &settings,
675 CdsHelperTerms {
676 start_date: Some(start_date),
677 ..CdsHelperTerms::default()
678 },
679 );
680
681 let schedule = expected_schedule(start_date, start_date + 1 + five_years());
682 assert_eq!(helper.earliest_date(), schedule.date(0));
683 assert_eq!(
684 helper.latest_date(),
685 Target::new().adjust(last_date(&schedule), BusinessDayConvention::Following)
686 );
687 assert_eq!(helper.protection_start(), today() + 1);
688 }
689
690 #[test]
694 fn an_evaluation_date_move_rebuilds_the_schedule() {
695 let settings = settings_at(today());
696 let helper = helper(&settings, CdsHelperTerms::default());
697 let (earliest, latest) = (helper.earliest_date(), helper.latest_date());
698
699 let moved = Date::new(15, Month::December, 2026);
700 settings.set_evaluation_date(moved);
701
702 assert_eq!(helper.protection_start(), moved + 1);
703 assert!(helper.earliest_date() > earliest);
704 assert!(helper.latest_date() > latest);
705 assert_eq!(
706 helper.earliest_date(),
707 Target::new().adjust(moved + 1, BusinessDayConvention::Following)
708 );
709 }
710
711 #[test]
715 fn the_post_big_bang_rules_are_refused() {
716 let settings = settings_at(today());
717 for rule in [
718 DateGeneration::CDS,
719 DateGeneration::CDS2015,
720 DateGeneration::OldCDS,
721 ] {
722 let result = SpreadCdsHelper::new(
723 Handle::new(shared(SimpleQuote::new(0.01)) as Shared<dyn Quote>),
724 five_years(),
725 1,
726 Target::new(),
727 Frequency::Quarterly,
728 BusinessDayConvention::Following,
729 rule,
730 Actual360::new(),
731 0.4,
732 discount(today()),
733 Shared::clone(&settings),
734 );
735 assert!(
736 result
737 .err()
738 .is_some_and(|error| { error.message().contains("cdsMaturity") })
739 );
740 }
741 }
742
743 #[test]
755 fn implied_quote_reprices_a_curve_it_does_not_observe() {
756 let settings = settings_at(today());
757 let helper = helper(&settings, CdsHelperTerms::default());
758
759 let hazard = shared(SimpleQuote::new(0.02));
760 let curve: Shared<dyn DefaultProbabilityTermStructure> = shared(FlatHazardRate::new(
761 today(),
762 Handle::new(Shared::clone(&hazard) as Shared<dyn Quote>),
763 Actual365Fixed::new(),
764 ));
765 helper.set_term_structure(&curve);
766
767 let first = helper.implied_quote().unwrap();
768 assert!(first.is_finite() && first > 0.0);
769 assert_eq!(helper.implied_quote().unwrap(), first);
770
771 hazard.set_value(0.05);
772 assert!(
773 helper
774 .swap
775 .borrow()
776 .as_ref()
777 .unwrap()
778 .base()
779 .is_calculated(),
780 "the weak link must leave the contract unnotified by the curve"
781 );
782
783 let second = helper.implied_quote().unwrap();
784 assert!(
785 second > first,
786 "a higher hazard rate must widen the fair spread, not repeat {first}"
787 );
788 }
789
790 #[test]
796 fn an_evaluation_date_move_rebuilds_the_contract_too() {
797 let settings = settings_at(today());
798 let helper = helper(&settings, CdsHelperTerms::default());
799 let curve: Shared<dyn DefaultProbabilityTermStructure> = shared(FlatHazardRate::with_rate(
800 today(),
801 0.02,
802 Actual365Fixed::new(),
803 ));
804 helper.set_term_structure(&curve);
805 helper.implied_quote().unwrap();
806
807 let moved = Date::new(15, Month::December, 2026);
808 settings.set_evaluation_date(moved);
809
810 let schedule = expected_schedule(moved + 1, moved + 1 + five_years());
811 let swap = helper.swap.borrow();
812 let swap = swap.as_ref().unwrap();
813 assert_eq!(swap.protection_start_date(), moved + 1);
814 assert_eq!(swap.maturity(), last_date(&schedule));
815 }
816
817 #[test]
823 fn a_discount_curve_move_notifies_the_helper() {
824 let settings = settings_at(today());
825 let rate = shared(SimpleQuote::new(0.03));
826 let discount_curve = Handle::new(shared(FlatForward::new(
827 today(),
828 Handle::new(Shared::clone(&rate) as Shared<dyn Quote>),
829 Actual365Fixed::new(),
830 Compounding::Continuous,
831 Frequency::Annual,
832 )) as Shared<dyn YieldTermStructure>);
833 let helper = SpreadCdsHelper::new(
834 Handle::new(shared(SimpleQuote::new(0.01)) as Shared<dyn Quote>),
835 five_years(),
836 1,
837 Target::new(),
838 Frequency::Quarterly,
839 BusinessDayConvention::Following,
840 DateGeneration::TwentiethIMM,
841 Actual360::new(),
842 0.4,
843 discount_curve,
844 Shared::clone(&settings),
845 )
846 .unwrap();
847
848 let flag = Flag::new();
849 helper.observable().register_observer(&as_observer(&flag));
850
851 rate.set_value(0.04);
852 assert!(Flag::is_up(&flag));
853 }
854
855 #[test]
858 fn implied_quote_without_a_curve_reports_the_missing_contract() {
859 let settings = settings_at(today());
860 let helper = helper(&settings, CdsHelperTerms::default());
861 assert!(
862 helper
863 .implied_quote()
864 .err()
865 .is_some_and(|error| error.message().contains("bootstrapping curve is set"))
866 );
867 }
868}