use crate::errors::QlResult;
use crate::fail;
use crate::handle::Handle;
use crate::math::interpolations::flat::BackwardFlat;
use crate::math::interpolations::loglinear::LogLinear;
use crate::termstructures::bootstraptraits::{Discount, ForwardRate};
use crate::termstructures::credit::defaulttermstructure::DefaultProbabilityTermStructure;
use crate::termstructures::credit::flathazardrate::FlatHazardRate;
use crate::termstructures::credit::interpolatedhazardratecurve::InterpolatedHazardRateCurve;
use crate::termstructures::credit::piecewisedefaultcurve::PiecewiseDefaultCurve;
use crate::termstructures::credit::probabilitytraits::HazardRate;
use crate::termstructures::yields::{
FlatForward, InterpolatedDiscountCurve, InterpolatedForwardCurve, PiecewiseYieldCurve,
};
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::date::Date;
pub fn isda_node_grid(
rate: &Handle<dyn YieldTermStructure>,
credit: &Handle<dyn DefaultProbabilityTermStructure>,
maturity: Date,
) -> QlResult<Vec<Date>> {
let rate_curve = rate.current_link()?;
let credit_curve = credit.current_link()?;
rate_curve.discount(0.0, false)?;
credit_curve.default_probability(0.0, false)?;
let mut nodes = yield_curve_dates(&*rate_curve)?;
nodes.extend(credit_curve_dates(&*credit_curve)?);
nodes.sort_unstable();
nodes.dedup();
if nodes.is_empty() {
nodes.push(maturity);
}
Ok(nodes)
}
fn yield_curve_dates(curve: &dyn YieldTermStructure) -> QlResult<Vec<Date>> {
const UNSUPPORTED: &str = "Yield curve must be flat forward interpolated";
let Some(any) = curve.as_any() else {
fail!("{UNSUPPORTED}");
};
if let Some(curve) = any.downcast_ref::<InterpolatedDiscountCurve<LogLinear>>() {
return Ok(curve.dates().to_vec());
}
if let Some(curve) = any.downcast_ref::<PiecewiseYieldCurve<Discount, LogLinear>>() {
return curve.dates();
}
if let Some(curve) = any.downcast_ref::<InterpolatedForwardCurve<BackwardFlat>>() {
return Ok(curve.dates().to_vec());
}
if let Some(curve) = any.downcast_ref::<PiecewiseYieldCurve<ForwardRate, BackwardFlat>>() {
return curve.dates();
}
if any.is::<FlatForward>() {
return Ok(Vec::new());
}
fail!("{UNSUPPORTED}")
}
fn credit_curve_dates(curve: &dyn DefaultProbabilityTermStructure) -> QlResult<Vec<Date>> {
const UNSUPPORTED: &str = "Credit curve must be flat forward interpolated";
let Some(any) = curve.as_any() else {
fail!("{UNSUPPORTED}");
};
if let Some(curve) = any.downcast_ref::<InterpolatedHazardRateCurve<BackwardFlat>>() {
return Ok(curve.dates().to_vec());
}
if let Some(curve) = any.downcast_ref::<PiecewiseDefaultCurve<HazardRate, BackwardFlat>>() {
return curve.dates();
}
if any.is::<FlatHazardRate>() {
return Ok(Vec::new());
}
fail!("{UNSUPPORTED}")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::indexes::ibor::euribor::Euribor;
use crate::interestrate::Compounding;
use crate::math::interpolations::linear::Linear;
use crate::patterns::observable::{AsObservable, Observable};
use crate::quotes::{Quote, SimpleQuote};
use crate::settings::Settings;
use crate::shared::{Shared, shared};
use crate::termstructures::bootstraptraits::Discount;
use crate::termstructures::credit::flathazardrate::FlatHazardRate;
use crate::termstructures::yields::{DepositRateHelper, FlatForward};
use crate::termstructures::{
RateHelper, TermStructure, TermStructureBase, yields::DiscountCurve,
};
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendars::target::Target;
use crate::time::date::{Month, SerialNumber};
use crate::time::daycounter::DayCounter;
use crate::time::daycounters::actual360::Actual360;
use crate::time::frequency::Frequency;
use crate::time::period::Period;
use crate::time::timeunit::TimeUnit;
use crate::types::{DiscountFactor, Time};
fn reference() -> Date {
Date::new(15, Month::June, 2026)
}
fn day_counter() -> DayCounter {
Actual360::new()
}
const YIELD_OFFSETS: [SerialNumber; 4] = [0, 90, 270, 540];
const CREDIT_OFFSETS: [SerialNumber; 4] = [0, 180, 270, 720];
fn dates_at(offsets: &[SerialNumber]) -> Vec<Date> {
offsets.iter().map(|days| reference() + *days).collect()
}
fn discount_curve() -> Shared<DiscountCurve> {
let dates = dates_at(&YIELD_OFFSETS);
let discounts = vec![1.0, 0.995, 0.985, 0.97];
shared(
DiscountCurve::new(dates, discounts, day_counter(), None)
.expect("the discount nodes are well formed"),
)
}
fn hazard_rate_curve() -> Shared<InterpolatedHazardRateCurve<BackwardFlat>> {
let dates = dates_at(&CREDIT_OFFSETS);
let rates = vec![0.01, 0.012, 0.014, 0.016];
shared(
InterpolatedHazardRateCurve::new(dates, rates, day_counter(), BackwardFlat)
.expect("the hazard-rate nodes are well formed"),
)
}
fn flat_forward() -> Shared<FlatForward> {
shared(FlatForward::with_rate(
reference(),
0.02,
day_counter(),
Compounding::Continuous,
Frequency::Annual,
))
}
fn flat_hazard_rate() -> Shared<FlatHazardRate> {
shared(FlatHazardRate::with_rate(reference(), 0.01, day_counter()))
}
fn yield_handle(
curve: Shared<impl YieldTermStructure + 'static>,
) -> Handle<dyn YieldTermStructure> {
Handle::new(curve as Shared<dyn YieldTermStructure>)
}
fn credit_handle(
curve: Shared<impl DefaultProbabilityTermStructure + 'static>,
) -> Handle<dyn DefaultProbabilityTermStructure> {
Handle::new(curve as Shared<dyn DefaultProbabilityTermStructure>)
}
fn maturity() -> Date {
reference() + 1000
}
#[test]
fn discount_curve_is_downcastable_to_its_pillar_dates() {
let curve = discount_curve();
let any = YieldTermStructure::as_any(&*curve).expect("the curve opts into the seam");
let recovered = any
.downcast_ref::<InterpolatedDiscountCurve<LogLinear>>()
.expect("the curve is log-linear interpolated");
assert_eq!(recovered.dates(), dates_at(&YIELD_OFFSETS));
}
#[test]
fn forward_curve_is_downcastable_to_its_pillar_dates() {
let dates = dates_at(&YIELD_OFFSETS);
let curve = shared(
InterpolatedForwardCurve::new(
dates.clone(),
vec![0.02, 0.021, 0.022, 0.023],
day_counter(),
BackwardFlat,
)
.expect("the forward nodes are well formed"),
);
let any = YieldTermStructure::as_any(&*curve).expect("the curve opts into the seam");
let recovered = any
.downcast_ref::<InterpolatedForwardCurve<BackwardFlat>>()
.expect("the curve is backward-flat interpolated");
assert_eq!(recovered.dates(), dates);
let grid = isda_node_grid(
&yield_handle(curve),
&credit_handle(flat_hazard_rate()),
maturity(),
)
.expect("a backward-flat forward curve is supported");
assert_eq!(
grid, dates,
"the arm is reached through the grid, not only directly"
);
}
#[test]
fn hazard_rate_curve_is_downcastable_to_its_pillar_dates() {
let curve = hazard_rate_curve();
let any =
DefaultProbabilityTermStructure::as_any(&*curve).expect("the curve opts into the seam");
let recovered = any
.downcast_ref::<InterpolatedHazardRateCurve<BackwardFlat>>()
.expect("the curve is backward-flat interpolated");
assert_eq!(recovered.dates(), dates_at(&CREDIT_OFFSETS));
}
#[test]
fn grid_is_the_sorted_deduplicated_union_of_both_curves() {
let grid = isda_node_grid(
&yield_handle(discount_curve()),
&credit_handle(hazard_rate_curve()),
maturity(),
)
.expect("both curves are supported shapes");
let expected = dates_at(&[0, 90, 180, 270, 540, 720]);
assert_eq!(grid.len(), expected.len(), "the two shared dates collapse");
assert_eq!(grid, expected);
assert!(
!grid.contains(&maturity()),
"the maturity is a fallback, not a grid point"
);
}
#[test]
fn flat_yield_curve_contributes_no_dates() {
let grid = isda_node_grid(
&yield_handle(flat_forward()),
&credit_handle(hazard_rate_curve()),
maturity(),
)
.expect("a flat forward curve is supported");
assert_eq!(grid, dates_at(&CREDIT_OFFSETS));
}
#[test]
fn flat_credit_curve_contributes_no_dates() {
let grid = isda_node_grid(
&yield_handle(discount_curve()),
&credit_handle(flat_hazard_rate()),
maturity(),
)
.expect("a flat hazard rate is supported");
assert_eq!(grid, dates_at(&YIELD_OFFSETS));
}
#[test]
fn two_flat_curves_give_the_maturity_alone() {
let grid = isda_node_grid(
&yield_handle(flat_forward()),
&credit_handle(flat_hazard_rate()),
maturity(),
)
.expect("both flat curves are supported");
assert_eq!(grid, vec![maturity()]);
}
#[test]
fn yield_curve_under_an_unsupported_interpolator_is_refused() {
let curve = InterpolatedDiscountCurve::<Linear>::new(
dates_at(&YIELD_OFFSETS),
vec![1.0, 0.995, 0.985, 0.97],
day_counter(),
None,
)
.expect("the discount nodes are well formed");
let error = isda_node_grid(
&yield_handle(shared(curve)),
&credit_handle(hazard_rate_curve()),
maturity(),
)
.expect_err("a linear discount curve is not an ISDA curve");
assert!(
error
.to_string()
.contains("Yield curve must be flat forward")
);
}
#[test]
fn credit_curve_under_an_unsupported_interpolator_is_refused() {
let curve = InterpolatedHazardRateCurve::<Linear>::new(
dates_at(&CREDIT_OFFSETS),
vec![0.01, 0.012, 0.014, 0.016],
day_counter(),
Linear,
)
.expect("the hazard-rate nodes are well formed");
let error = isda_node_grid(
&yield_handle(discount_curve()),
&credit_handle(shared(curve)),
maturity(),
)
.expect_err("a linear hazard-rate curve is not an ISDA curve");
assert!(
error
.to_string()
.contains("Credit curve must be flat forward")
);
}
#[test]
fn curve_outside_the_seam_is_refused() {
let error = isda_node_grid(
&yield_handle(shared(SpreadedCurve::new())),
&credit_handle(hazard_rate_curve()),
maturity(),
)
.expect_err("a curve outside the seam is not an ISDA curve");
assert!(
error
.to_string()
.contains("Yield curve must be flat forward")
);
}
#[test]
fn bootstrapped_discount_curve_contributes_its_solved_pillars() {
let settings = shared(Settings::<Date>::new());
let today = Target::new().adjust(reference(), BusinessDayConvention::Following);
settings.set_evaluation_date(today);
let tenors = [(3, TimeUnit::Months), (6, TimeUnit::Months)];
let helpers: Vec<Shared<dyn RateHelper>> = tenors
.iter()
.map(|(n, units)| {
let quote = Handle::new(shared(SimpleQuote::new(0.04)) as Shared<dyn Quote>);
let index =
Euribor::new(Period::new(*n, *units), Handle::empty(), settings.clone())
.expect("the deposit tenor is valid");
DepositRateHelper::new(quote, &index) as Shared<dyn RateHelper>
})
.collect();
let curve = PiecewiseYieldCurve::<Discount, LogLinear>::new(
today,
helpers,
day_counter(),
LogLinear,
)
.expect("the deposit helpers bootstrap");
let pillars = curve.dates().expect("the bootstrap succeeds");
assert_eq!(pillars.len(), 3, "the reference date plus the two deposits");
let grid = isda_node_grid(
&yield_handle(curve),
&credit_handle(flat_hazard_rate()),
maturity(),
)
.expect("a bootstrapped log-linear discount curve is supported");
assert_eq!(grid, pillars);
}
struct SpreadedCurve {
base: TermStructureBase,
}
impl SpreadedCurve {
fn new() -> SpreadedCurve {
SpreadedCurve {
base: TermStructureBase::with_reference_date(
reference(),
None,
Some(day_counter()),
),
}
}
}
impl AsObservable for SpreadedCurve {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl TermStructure for SpreadedCurve {
fn base(&self) -> &TermStructureBase {
&self.base
}
fn max_date(&self) -> Date {
Date::max_date()
}
}
impl YieldTermStructure for SpreadedCurve {
fn discount_impl(&self, _t: Time) -> QlResult<DiscountFactor> {
Ok(1.0)
}
}
}