use std::cell::RefCell;
use crate::errors::{QlError, QlResult};
use crate::math::interpolations::Interpolator;
use crate::math::solver1d::Solver1D;
use crate::math::solvers1d::brent::Brent;
use crate::math::solvers1d::finitedifferencenewtonsafe::FiniteDifferenceNewtonSafe;
use crate::patterns::observable::Observable;
use crate::shared::Shared;
use crate::termstructures::bootstraphelper::{BootstrapHelperShared, sort_by_pillar_date};
use crate::termstructures::bootstraptraits::{BootstrapTraits, CurveData};
use crate::time::date::Date;
use crate::types::{Real, Size, Time};
use crate::{fail, require};
mod options;
pub use options::IterativeBootstrapOptions;
pub trait PiecewiseCurve {
type Traits: BootstrapTraits;
type Interp: Interpolator;
type TS: ?Sized;
type Helper: BootstrapHelperShared<TS = Self::TS> + ?Sized;
fn instruments(&self) -> &[Shared<Self::Helper>];
fn interpolator(&self) -> &Self::Interp;
fn curve_data(&self) -> &RefCell<CurveData<Self::Interp>>;
fn accuracy(&self) -> Real;
fn reference_date(&self) -> QlResult<Date>;
fn initial_date(&self) -> QlResult<Date> {
self.reference_date()
}
fn initial_value(&self) -> QlResult<Real> {
Ok(Self::Traits::initial_value())
}
fn time_from_reference(&self, date: Date) -> QlResult<Time>;
fn term_structure_shared(&self) -> QlResult<Shared<Self::TS>>;
}
pub trait Bootstrap<C: PiecewiseCurve> {
fn register_helper_owner(&self, _owner: std::rc::Weak<C::TS>) {}
fn calculate(&self, curve: &C) -> QlResult<()>;
fn additional_observables(&self) -> Vec<Shared<Observable>> {
Vec::new()
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct IterativeBootstrap {
options: IterativeBootstrapOptions,
}
impl IterativeBootstrap {
pub fn new() -> IterativeBootstrap {
Self::default()
}
pub fn with_options(options: IterativeBootstrapOptions) -> QlResult<Self> {
options.validate()?;
Ok(Self { options })
}
pub fn calculate<C: PiecewiseCurve>(&self, curve: &C) -> QlResult<()> {
let mut helpers: Vec<Shared<C::Helper>> = curve.instruments().to_vec();
let n = helpers.len();
require!(n > 0, "no bootstrap helpers given");
sort_by_pillar_date(&mut helpers);
let first_date = curve.initial_date()?;
let initial_value = curve.initial_value()?;
require!(
helpers[n - 1].pillar_date() > first_date,
"all instruments expired"
);
let mut first_alive = 0usize;
while helpers[first_alive].pillar_date() <= first_date {
first_alive += 1;
}
let alive = n - first_alive;
let nodes = alive + 1;
let required = curve.interpolator().required_points();
require!(
nodes >= required,
"not enough alive instruments: {alive} provided, {} required",
required - 1
);
let mut dates = Vec::with_capacity(nodes);
let mut times = Vec::with_capacity(nodes);
dates.push(first_date);
times.push(curve.time_from_reference(first_date)?);
let mut max_date = first_date;
let mut loop_required = C::Interp::GLOBAL;
for (i, j) in (1..).zip(first_alive..n) {
let pillar = helpers[j].pillar_date();
require!(
dates[i - 1] != pillar,
"more than one instrument with pillar {pillar}"
);
let latest_relevant = helpers[j].latest_relevant_date();
require!(
latest_relevant > max_date,
"instrument with pillar {pillar} has latest-relevant date \
{latest_relevant} before or equal to a previous instrument's ({max_date})"
);
dates.push(pillar);
times.push(curve.time_from_reference(pillar)?);
max_date = pillar.max(latest_relevant);
if pillar < latest_relevant {
loop_required = true;
}
}
let mut valid_data = {
let mut cd = curve.curve_data().borrow_mut();
let reuse = cd.is_valid() && cd.data().len() == nodes;
cd.set_pillars(dates, times);
if !reuse {
cd.reset_data(initial_value, nodes);
}
cd.set_max_date(max_date);
cd.set_valid(false);
reuse
};
let term_structure = curve.term_structure_shared()?;
for helper in helpers.iter().take(n).skip(first_alive) {
helper.quote_value()?;
helper.set_term_structure(&term_structure);
}
let cached_valid = valid_data;
let accuracy = self.options.accuracy.unwrap_or_else(|| curve.accuracy());
require!(
accuracy.is_finite() && accuracy > 0.0,
"invalid bootstrap accuracy"
);
let max_iterations = C::Traits::max_iterations() - 1;
let mut previous_data: Vec<Real> = Vec::new();
for iteration in 0.. {
if loop_required && valid_data {
previous_data = curve.curve_data().borrow().data().to_vec();
}
for (i, j) in (1..).zip(first_alive..n) {
let (mut min, mut max) = {
let cd = curve.curve_data().borrow();
(
self.options.min_value.unwrap_or_else(|| {
C::Traits::min_value_after(i, cd.times(), cd.data(), valid_data)
}),
self.options.max_value.unwrap_or_else(|| {
C::Traits::max_value_after(i, cd.times(), cd.data(), valid_data)
}),
)
};
let upto = if valid_data { alive } else { i };
let helper = &helpers[j];
let mut attempt = 1;
let mut used_fallback = false;
let root = loop {
if cached_valid
&& !(min.is_finite()
&& max.is_finite()
&& min < max
&& (max - min).is_finite())
{
return self.calculate(curve);
}
require!(
min.is_finite() && max.is_finite() && min < max && (max - min).is_finite(),
"invalid bootstrap bracket [{min}, {max}]"
);
let mut guess = {
let cd = curve.curve_data().borrow();
C::Traits::guess(i, cd.times(), cd.data(), valid_data)
};
if guess >= max {
guess = max - (max - min) / 5.0;
} else if guess <= min {
guess = min + (max - min) / 5.0;
}
let error_slot = RefCell::new(None);
let error = |g: Real| match node_error::<C>(curve, i, upto, helper, g) {
Ok(value) if value.is_finite() => value,
result => {
*error_slot.borrow_mut() = Some(result.err().unwrap_or_else(|| {
QlError::new("nonfinite bootstrap quote error", file!(), line!())
}));
Real::NAN
}
};
let solved = if valid_data {
FiniteDifferenceNewtonSafe::new()
.with_max_evaluations(self.options.max_evaluations)
.solve_bracketed(error, accuracy, guess, min, max)
} else {
Brent::new()
.with_max_evaluations(self.options.max_evaluations)
.solve_bracketed(error, accuracy, guess, min, max)
};
let solved = match error_slot.into_inner() {
Some(error) => Err(error),
None => solved,
};
match solved {
Ok(root) => {
require!(root.is_finite(), "nonfinite bootstrap solver root");
break root;
}
Err(_) if cached_valid => return self.calculate(curve),
Err(_) if attempt < self.options.max_attempts => {
attempt += 1;
min = if min < 0.0 {
min * self.options.min_factor
} else {
min / self.options.min_factor
};
max = if max > 0.0 {
max * self.options.max_factor
} else {
max / self.options.max_factor
};
}
Err(_) if self.options.dont_throw => {
used_fallback = true;
break fallback(
|g| node_error::<C>(curve, i, upto, helper, g),
min,
max,
self.options.dont_throw_steps,
)?;
}
Err(error) => fail!(
"bootstrap failed at pillar {} (maturity {}): {}",
helper.pillar_date(),
helper.maturity_date(),
error.message()
),
}
};
let mut cd = curve.curve_data().borrow_mut();
if used_fallback {
cd.data_mut()[i] = root;
} else {
C::Traits::update_guess(cd.data_mut(), root, i);
}
cd.rebuild(curve.interpolator(), upto)?;
}
if !loop_required {
break;
}
let mut change: Real = 0.0;
if valid_data {
let cd = curve.curve_data().borrow();
let data = cd.data();
for i in 1..=alive {
change = change.max((data[i] - previous_data[i]).abs());
}
if change <= accuracy {
break;
}
}
if iteration == max_iterations && self.options.dont_throw {
break;
}
require!(
iteration != max_iterations,
"convergence not reached after {iteration} iterations; \
last improvement {change}, required accuracy {accuracy}"
);
valid_data = true;
}
curve.curve_data().borrow_mut().set_valid(true);
Ok(())
}
}
fn fallback(
mut error: impl FnMut(Real) -> QlResult<Real>,
min: Real,
max: Real,
steps: Size,
) -> QlResult<Real> {
let mut best = min;
let mut best_error = error(min)?.abs();
require!(best_error.is_finite(), "nonfinite bootstrap fallback error");
let step = (max - min) / steps as Real;
let mut value = min;
for _ in 0..steps {
value += step;
let current_error = error(value)?.abs();
require!(
current_error.is_finite(),
"nonfinite bootstrap fallback error"
);
if current_error < best_error {
best = value;
best_error = current_error;
}
}
Ok(best)
}
impl<C: PiecewiseCurve> Bootstrap<C> for IterativeBootstrap {
fn calculate(&self, curve: &C) -> QlResult<()> {
IterativeBootstrap::calculate(self, curve)
}
}
fn node_error<C: PiecewiseCurve>(
curve: &C,
i: Size,
upto: Size,
helper: &Shared<C::Helper>,
guess: Real,
) -> QlResult<Real> {
{
let mut cd = curve.curve_data().borrow_mut();
C::Traits::update_guess(cd.data_mut(), guess, i);
cd.rebuild(curve.interpolator(), upto)?;
}
helper.quote_error()
}
#[cfg(test)]
mod tests {
use std::rc::Weak;
use super::*;
use crate::handle::Handle;
use crate::indexes::ibor::euribor::Euribor;
use crate::math::interpolations::loglinear::LogLinear;
use crate::patterns::observable::{AsObservable, Observable};
use crate::settings::Settings;
use crate::shared::shared;
use crate::termstructures::bootstraphelper::RateHelper;
use crate::termstructures::bootstraptraits::{Discount, YieldBootstrapTraits};
use crate::termstructures::yields::DepositRateHelper;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::termstructures::{TermStructure, TermStructureBase};
use crate::time::date::{Date, Month};
use crate::time::daycounters::actual360::Actual360;
use crate::time::period::Period;
use crate::time::timeunit::TimeUnit;
use crate::types::DiscountFactor;
struct StubCurve {
base: TermStructureBase,
instruments: Vec<Shared<dyn RateHelper>>,
interpolator: LogLinear,
data: RefCell<CurveData<LogLinear>>,
self_weak: Weak<dyn YieldTermStructure>,
initial_date: Option<Date>,
initial_value: Option<Real>,
}
impl StubCurve {
fn new(reference: Date, instruments: Vec<Shared<dyn RateHelper>>) -> Shared<StubCurve> {
StubCurve::build(reference, instruments, None, None)
}
fn with_initial_date(
reference: Date,
instruments: Vec<Shared<dyn RateHelper>>,
initial_date: Date,
) -> Shared<StubCurve> {
StubCurve::build(reference, instruments, Some(initial_date), None)
}
fn with_initial_value(
reference: Date,
instruments: Vec<Shared<dyn RateHelper>>,
initial_value: Real,
) -> Shared<StubCurve> {
StubCurve::build(reference, instruments, None, Some(initial_value))
}
fn build(
reference: Date,
instruments: Vec<Shared<dyn RateHelper>>,
initial_date: Option<Date>,
initial_value: Option<Real>,
) -> Shared<StubCurve> {
Shared::new_cyclic(|weak: &Weak<StubCurve>| {
let self_weak: Weak<dyn YieldTermStructure> = weak.clone();
StubCurve {
base: TermStructureBase::with_reference_date(
reference,
None,
Some(Actual360::new()),
),
instruments,
interpolator: LogLinear,
data: RefCell::new(CurveData::new()),
self_weak,
initial_date,
initial_value,
}
})
}
}
impl AsObservable for StubCurve {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl TermStructure for StubCurve {
fn base(&self) -> &TermStructureBase {
&self.base
}
fn max_date(&self) -> Date {
self.data
.borrow()
.max_date()
.unwrap_or_else(|| self.base.reference_date().expect("fixed reference date"))
}
}
impl YieldTermStructure for StubCurve {
fn discount_impl(&self, t: Time) -> QlResult<DiscountFactor> {
let data = self.data.borrow();
Discount::discount_from_nodes(data.interpolation()?, t)
}
}
impl PiecewiseCurve for StubCurve {
type Traits = Discount;
type Interp = LogLinear;
type TS = dyn YieldTermStructure;
type Helper = dyn RateHelper;
fn instruments(&self) -> &[Shared<dyn RateHelper>] {
&self.instruments
}
fn interpolator(&self) -> &LogLinear {
&self.interpolator
}
fn curve_data(&self) -> &RefCell<CurveData<LogLinear>> {
&self.data
}
fn accuracy(&self) -> Real {
1.0e-12
}
fn reference_date(&self) -> QlResult<Date> {
self.base.reference_date()
}
fn initial_date(&self) -> QlResult<Date> {
match self.initial_date {
Some(date) => Ok(date),
None => self.base.reference_date(),
}
}
fn initial_value(&self) -> QlResult<Real> {
match self.initial_value {
Some(value) => Ok(value),
None => Ok(Discount::initial_value()),
}
}
fn time_from_reference(&self, date: Date) -> QlResult<Time> {
TermStructure::time_from_reference(self, date)
}
fn term_structure_shared(&self) -> QlResult<Shared<dyn YieldTermStructure>> {
self.self_weak
.upgrade()
.ok_or_else(|| QlError::new("curve dropped", file!(), line!()))
}
}
fn settings_on(today: Date) -> Shared<Settings<Date>> {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(today);
settings
}
fn euribor(
tenor: Period,
settings: Shared<Settings<Date>>,
) -> crate::indexes::iborindex::IborIndex {
Euribor::new(tenor, Handle::empty(), settings).expect("month tenor is valid")
}
#[test]
fn bootstrap_reproduces_deposit_quotes() {
let today = Date::new(15, Month::June, 2026);
let settings = settings_on(today);
let three_m = euribor(Period::new(3, TimeUnit::Months), settings.clone());
let six_m = euribor(Period::new(6, TimeUnit::Months), settings.clone());
let nine_m = euribor(Period::new(9, TimeUnit::Months), settings.clone());
let h3 = DepositRateHelper::from_rate(0.04557, &three_m);
let h6 = DepositRateHelper::from_rate(0.04496, &six_m);
let h9 = DepositRateHelper::from_rate(0.04490, &nine_m);
let reference = h3.earliest_date();
let instruments: Vec<Shared<dyn RateHelper>> = vec![
Shared::clone(&h3) as Shared<dyn RateHelper>,
Shared::clone(&h6) as Shared<dyn RateHelper>,
Shared::clone(&h9) as Shared<dyn RateHelper>,
];
let curve = StubCurve::new(reference, instruments);
IterativeBootstrap::new().calculate(curve.as_ref()).unwrap();
for helper in [&h3, &h6, &h9] {
let error = helper.quote_error().unwrap();
assert!(error.abs() < 1.0e-12, "deposit quote error {error}");
}
}
#[test]
fn the_first_node_sits_at_the_initial_date_which_may_precede_the_reference() {
let today = Date::new(15, Month::June, 2026);
let settings = settings_on(today);
let three_m = euribor(Period::new(3, TimeUnit::Months), settings.clone());
let six_m = euribor(Period::new(6, TimeUnit::Months), settings.clone());
let h3 = DepositRateHelper::from_rate(0.04557, &three_m);
let h6 = DepositRateHelper::from_rate(0.04496, &six_m);
let reference = h3.earliest_date();
let base_date = reference - 90;
let instruments: Vec<Shared<dyn RateHelper>> = vec![
Shared::clone(&h3) as Shared<dyn RateHelper>,
Shared::clone(&h6) as Shared<dyn RateHelper>,
];
let curve = StubCurve::with_initial_date(reference, instruments, base_date);
IterativeBootstrap::new().calculate(curve.as_ref()).unwrap();
let data = curve.data.borrow();
assert_eq!(data.dates()[0], base_date);
assert!(data.times()[0] < 0.0, "times[0] = {}", data.times()[0]);
assert_eq!(data.times()[0], -90.0 / 360.0);
drop(data);
for helper in [&h3, &h6] {
let error = helper.quote_error().unwrap();
assert!(error.abs() < 1.0e-12, "deposit quote error {error}");
}
}
#[test]
fn node_zero_is_seeded_from_the_curves_initial_value_hook() {
let today = Date::new(15, Month::June, 2026);
let settings = settings_on(today);
let three_m = euribor(Period::new(3, TimeUnit::Months), settings.clone());
let six_m = euribor(Period::new(6, TimeUnit::Months), settings.clone());
let h3 = DepositRateHelper::from_rate(0.04557, &three_m);
let h6 = DepositRateHelper::from_rate(0.04496, &six_m);
let reference = h3.earliest_date();
let instruments: Vec<Shared<dyn RateHelper>> = vec![
Shared::clone(&h3) as Shared<dyn RateHelper>,
Shared::clone(&h6) as Shared<dyn RateHelper>,
];
let seed = 0.42;
assert_ne!(seed, Discount::initial_value());
let curve = StubCurve::with_initial_value(reference, instruments, seed);
IterativeBootstrap::new().calculate(curve.as_ref()).unwrap();
assert_eq!(curve.data.borrow().data()[0], seed);
for helper in [&h3, &h6] {
let error = helper.quote_error().unwrap();
assert!(error.abs() < 1.0e-12, "deposit quote error {error}");
}
}
#[test]
fn fallback_keeps_first_tie_and_rejects_nonfinite_evaluations() {
assert_eq!(fallback(|_| Ok(1.0), -1.0, 1.0, 10).unwrap(), -1.0);
assert!(fallback(|_| Ok(Real::NAN), -1.0, 1.0, 10).is_err());
assert!(
fallback(
|x| if x > 0.0 { Ok(Real::INFINITY) } else { Ok(x) },
-1.0,
1.0,
10
)
.is_err()
);
}
#[test]
fn empty_helper_set_is_rejected() {
let today = Date::new(15, Month::June, 2026);
let _ = settings_on(today);
let curve = StubCurve::new(today, Vec::new());
let err = IterativeBootstrap::new()
.calculate(curve.as_ref())
.unwrap_err();
assert!(err.message().contains("no bootstrap helpers"));
}
}