use std::{cell::RefCell, collections::HashMap, rc::Rc};
use nalgebra::{DMatrix, DVector};
use crate::{
ad::{dual::DualFwd, scalar::Scalar},
calibration::{
calibrationpricer::CalibrationInstrumentPricer, calibrationprocess::CalibrationProcess,
},
core::{
elements::curveelement::{ADCurveElement, DiscountCurveElement},
marketdatahandling::constructedelementstore::SharedElement,
},
indices::marketindex::MarketIndex,
math::{
interpolation::interpolator::Interpolator,
solvers::{
solvertraits::{ContFunc, JacobianFunc, VectorFunc},
vectornewton::VectorNewton,
},
},
quotes::{
calibrationinstrument::CalibrationInstrument, fxstore::FxStore, quote::Level,
quoteselector::QuoteSelector,
},
rates::{
bootstrapping::{
bootstrapcalibrationinstrument::BootstrapStepEvaluation,
bootstrapdiscountpolicy::BootstrapDiscountPolicy,
bootstrappedcurve::BootstrappedCurve,
bootstrapstep::BootstrapStep,
bootstraputils::{dependency_order, CrossCurveDep},
curveconfiguration::CurveConfiguration,
},
yieldtermstructure::discounttermstructure::DiscountTermStructure,
},
time::{date::Date, daycounter::DayCounter},
utils::errors::{QSError, Result},
};
pub struct MultiCurveBootstrapper {
curve_specs: Vec<CurveConfiguration>,
discount_policy: BootstrapDiscountPolicy,
fx_store: FxStore,
}
impl MultiCurveBootstrapper {
#[must_use]
pub fn new(
curve_specs: Vec<CurveConfiguration>,
discount_policy: BootstrapDiscountPolicy,
) -> Self {
Self {
curve_specs,
discount_policy,
fx_store: FxStore::new(),
}
}
#[must_use]
pub fn with_fx_store(mut self, store: FxStore) -> Self {
self.fx_store = store;
self
}
pub fn bootstrap(
&self,
selector: &impl QuoteSelector,
level: Level,
) -> Result<HashMap<MarketIndex, DiscountCurveElement>> {
let mut resolved = HashMap::new();
for spec in &self.curve_specs {
let mut resolved_spec = (*spec).clone();
let fx_spot = if let MarketIndex::Collateral(ccy, coll_ccy) = spec.market_index() {
self.fx_store
.get_fx_rate(*coll_ccy, *ccy)
.ok()
.map(|r| r.value())
} else {
None
};
resolved_spec.resolve(selector, level, fx_spot)?;
resolved.insert(resolved_spec.market_index().clone(), resolved_spec);
}
let order = dependency_order(&resolved, &self.discount_policy)?;
let mut solved_curves: HashMap<MarketIndex, BootstrappedCurve> = HashMap::new();
let mut pillar_values: HashMap<MarketIndex, Vec<DualFwd>> = HashMap::new();
for index in &order {
let spec = resolved.get(index).ok_or_else(|| {
QSError::NotFoundErr(format!("Missing resolved spec for {index}"))
})?;
let calibrated = self.bootstrap_next_curve(index, spec, &solved_curves)?;
let calibrated_pillar_values = calibrated.pillar_values()?.to_vec();
solved_curves.insert(index.clone(), calibrated);
pillar_values.insert(index.clone(), calibrated_pillar_values);
}
let mut result = HashMap::new();
for index in &order {
let sc = solved_curves
.get(index)
.ok_or_else(|| QSError::NotFoundErr(format!("Missing solved curve for {index}")))?;
let spec = resolved.get(index).ok_or_else(|| {
QSError::NotFoundErr(format!("Missing resolved spec for {index}"))
})?;
let pv = pillar_values.get(index).ok_or_else(|| {
QSError::NotFoundErr(format!("Missing pillar values for {index}"))
})?;
let reference_date = spec.reference_date()?;
let mut dates = vec![reference_date];
dates.extend(spec.pillar_dates());
let ad_dfs = sc.output_discount_factors().map_or_else(
|_| {
sc.discount_factors()
.iter()
.map(|&df| DualFwd::new(df))
.collect()
},
<[DualFwd]>::to_vec,
);
let mut ts = DiscountTermStructure::<DualFwd>::new(
dates,
ad_dfs,
spec.day_counter(),
spec.interpolator(),
spec.enable_extrapolation(),
)?;
ts = ts.with_pillar_values(pv.clone())?;
let labels = sc
.pillar_labels()
.map_or_else(|| spec.pillar_labels(), <[String]>::to_vec);
ts = ts.with_pillar_labels(labels)?;
if let Some(ift_sens) = sc.ift_sensitivities() {
ts = ts.with_ift_sensitivities(ift_sens.clone());
}
let shared: SharedElement<dyn ADCurveElement> = Rc::new(RefCell::new(ts));
let elem = DiscountCurveElement::new(index.clone(), shared);
result.insert(index.clone(), elem);
}
Ok(result)
}
#[allow(clippy::too_many_lines)]
fn bootstrap_next_curve(
&self,
target_index: &MarketIndex,
curve_config: &CurveConfiguration,
other_curves: &HashMap<MarketIndex, BootstrappedCurve>,
) -> Result<BootstrappedCurve> {
let reference_date = curve_config.reference_date()?;
let dc = curve_config.day_counter();
let interp = curve_config.interpolator();
let instruments = curve_config.instruments()?;
let mut times = vec![0.0_f64];
times.extend(
instruments
.iter()
.map(|instr| dc.year_fraction(reference_date, instr.pillar_date())),
);
let n = instruments.len();
let x0 = vec![0.99; n];
let problem = BootstrapObjectiveFunc {
target_index: target_index.clone(),
reference_date,
times: times.clone(),
day_counter: dc,
interpolator: interp,
instruments,
other_curves,
discount_policy: &self.discount_policy,
fx_store: &self.fx_store,
};
let solver = VectorNewton::new(1e-12, 200);
let solution = solver.solve(&problem, &x0)?;
let converged_x = &solution.x;
let mut solved_dfs = vec![1.0_f64];
solved_dfs.extend(converged_x.iter().copied());
let quote_vals = curve_config.quote_values();
let j_raw = solution
.jacobian
.ok_or_else(|| QSError::SolverErr("Newton solver did not return a Jacobian".into()))?;
let g_diag = Self::compute_quote_sensitivities(&problem, converged_x)?;
let j_data: Vec<f64> = j_raw.iter().flat_map(|row| row.iter().copied()).collect();
let j_mat = DMatrix::from_row_slice(n, n, &j_data);
let lu = j_mat.lu();
let mut sensitivity = vec![vec![0.0_f64; n]; n];
for j in 0..n {
let mut rhs = DVector::zeros(n);
rhs[j] = g_diag[j];
if let Some(col) = lu.solve(&rhs) {
for i in 0..n {
sensitivity[i][j] = -col[i];
}
}
}
let _base_residual = problem.call(converged_x)?;
let mut cross_deps: Vec<CrossCurveDep> = Vec::new();
for (parent_idx, parent_curve) in other_curves {
let parent_dfs = parent_curve.discount_factors();
let parent_n = parent_dfs.len() - 1;
let parent_ift = match parent_curve.ift_sensitivities() {
Some(ift) => ift.clone(),
None => continue,
};
let parent_labels: Vec<String> = parent_curve.pillar_labels().map_or_else(
|| {
parent_curve
.pillar_values()
.map(|pv| pv.iter().map(|_| String::new()).collect())
.unwrap_or_default()
},
<[String]>::to_vec,
);
let mut df_dz = vec![vec![0.0_f64; parent_n]; n];
for m in 0..parent_n {
let bump = (parent_dfs[m + 1].abs() * 1e-6).max(1e-10);
let (up_res, up_bump) =
bumped_residual(&problem, parent_idx, parent_curve, m + 1, bump, converged_x)?;
let (dn_res, dn_bump) = bumped_residual(
&problem,
parent_idx,
parent_curve,
m + 1,
-bump,
converged_x,
)?;
let denom = up_bump - dn_bump;
for row in 0..n {
df_dz[row][m] = (up_res[row] - dn_res[row]) / denom;
}
}
let mut cross_df_sens = vec![vec![0.0_f64; parent_n]; n];
let mut has_nonzero = false;
for m in 0..parent_n {
let mut rhs = DVector::zeros(n);
for row in 0..n {
rhs[row] = df_dz[row][m];
}
if let Some(col) = lu.solve(&rhs) {
for i in 0..n {
let val = -col[i];
if val.abs() > 1e-16 {
cross_df_sens[i][m] = val;
has_nonzero = true;
}
}
}
}
if has_nonzero {
let parent_quote_vals: Vec<f64> = parent_curve
.pillar_values()
.map(|pv| pv.iter().map(Scalar::value).collect())
.unwrap_or_default();
cross_deps.push(CrossCurveDep {
cross_df_sens,
parent_ift_sens: parent_ift,
parent_quote_values: parent_quote_vals,
parent_pillar_labels: parent_labels,
});
}
}
let quote_ad: Vec<DualFwd> = quote_vals.iter().map(|&v| DualFwd::new(v)).collect();
let mut full_sensitivity = sensitivity.clone();
let mut full_quotes = quote_ad;
let mut full_labels = curve_config.pillar_labels();
for dep in &cross_deps {
let parent_n_quotes = dep.parent_quote_values.len();
let m_count = dep.parent_ift_sens.len();
for (i, sensitivity_row) in full_sensitivity.iter_mut().enumerate().take(n) {
let mut row_ext = Vec::with_capacity(parent_n_quotes);
for k in 0..parent_n_quotes {
let mut combined = 0.0_f64;
for m in 0..m_count {
combined = f64::mul_add(dep.cross_df_sens[i][m], dep.parent_ift_sens[m][k], combined);
}
row_ext.push(combined);
}
sensitivity_row.extend(row_ext);
}
for &v in &dep.parent_quote_values {
full_quotes.push(DualFwd::new(v));
}
full_labels.extend(dep.parent_pillar_labels.clone());
}
let n_total = full_quotes.len();
let mut ad_dfs: Vec<DualFwd> = Vec::with_capacity(n + 1);
ad_dfs.push(DualFwd::new(1.0)); for i in 0..n {
let mut df_ad = DualFwd::new(converged_x[i]);
for j in 0..n_total {
let s = full_sensitivity[i][j];
if s.abs() > 1e-16 {
let delta = full_quotes[j] - DualFwd::new(full_quotes[j].value());
df_ad = (df_ad + DualFwd::new(s) * delta).into();
}
}
ad_dfs.push(df_ad);
}
Ok(BootstrappedCurve::new(
target_index.clone(),
reference_date,
times,
solved_dfs,
dc,
interp,
)
.with_pillar_values(full_quotes)
.with_pillar_labels(full_labels)
.with_output_discount_factors(ad_dfs)
.with_ift_sensitivities(full_sensitivity))
}
fn compute_quote_sensitivities(
problem: &BootstrapObjectiveFunc,
x: &[f64],
) -> Result<Vec<f64>> {
let trial = problem.create_trial_curve(x);
let step = BootstrapStep::new(
&trial,
problem.other_curves,
problem.discount_policy,
problem.fx_store,
);
let evaluator = BootstrapStepEvaluation::new(&step);
problem
.instruments
.iter()
.map(|inst| evaluator.sensitivity(inst))
.collect()
}
}
fn bumped_residual(
problem: &BootstrapObjectiveFunc,
parent_idx: &MarketIndex,
parent_curve: &BootstrappedCurve,
parent_df_idx: usize,
bump: f64,
x: &[f64],
) -> Result<(Vec<f64>, f64)> {
let original_df = parent_curve.discount_factors()[parent_df_idx];
let bumped_df = (original_df + bump).max(1e-10);
let mut bumped_parent = parent_curve.clone();
bumped_parent.discount_factors_mut()[parent_df_idx] = bumped_df;
let mut bumped_others = problem.other_curves.clone();
bumped_others.insert(parent_idx.clone(), bumped_parent);
let bumped_problem = BootstrapObjectiveFunc {
target_index: problem.target_index.clone(),
reference_date: problem.reference_date,
times: problem.times.clone(),
day_counter: problem.day_counter,
interpolator: problem.interpolator,
instruments: problem.instruments,
other_curves: &bumped_others,
discount_policy: problem.discount_policy,
fx_store: problem.fx_store,
};
Ok((bumped_problem.call(x)?, bumped_df - original_df))
}
struct BootstrapObjectiveFunc<'a> {
pub target_index: MarketIndex,
pub reference_date: Date,
pub times: Vec<f64>,
pub day_counter: DayCounter,
pub interpolator: Interpolator,
pub instruments: &'a [CalibrationInstrument],
pub other_curves: &'a HashMap<MarketIndex, BootstrappedCurve>,
pub discount_policy: &'a BootstrapDiscountPolicy,
pub fx_store: &'a FxStore,
}
impl BootstrapObjectiveFunc<'_> {
fn create_trial_curve(&self, x: &[f64]) -> BootstrappedCurve {
let mut dfs = Vec::with_capacity(self.times.len());
dfs.push(1.0_f64); dfs.extend_from_slice(x);
BootstrappedCurve::new(
self.target_index.clone(),
self.reference_date,
self.times.clone(),
dfs,
self.day_counter,
self.interpolator,
)
}
}
impl ContFunc<[f64], Vec<f64>> for BootstrapObjectiveFunc<'_> {
fn call(&self, x: &[f64]) -> Result<Vec<f64>> {
let trial = self.create_trial_curve(x);
let step = BootstrapStep::new(
&trial,
self.other_curves,
self.discount_policy,
self.fx_store,
);
let evaluator = BootstrapStepEvaluation::new(&step);
evaluator.residual(self.instruments)
}
}
impl JacobianFunc<f64, f64, f64> for BootstrapObjectiveFunc<'_> {
fn jacobian(&self, x: &[f64]) -> Result<Vec<Vec<f64>>> {
let n = x.len();
let mut jacobian = vec![vec![0.0; n]; n];
for col in 0..n {
let base_bump = (x[col].abs().max(1.0) * 1e-6).max(1e-8);
let bump = base_bump.min((x[col] * 0.25).max(1e-8));
let mut up = x.to_vec();
let mut dn = x.to_vec();
up[col] += bump;
dn[col] = (dn[col] - bump).max(1e-8);
let up_res = self.call(&up)?;
let dn_res = self.call(&dn)?;
let denom = up[col] - dn[col];
for row in 0..n {
jacobian[row][col] = (up_res[row] - dn_res[row]) / denom;
}
}
Ok(jacobian)
}
}
impl VectorFunc<f64, f64> for BootstrapObjectiveFunc<'_> {}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use crate::{
ad::dual::DualFwd,
currencies::currency::Currency,
indices::marketindex::MarketIndex,
math::interpolation::interpolator::Interpolator,
quotes::{
fxstore::FxStore,
quote::{Level, Quote, QuoteDetails, QuoteLevels},
quoteselector::QuoteSelector,
},
rates::bootstrapping::{
bootstrapdiscountpolicy::BootstrapDiscountPolicy,
curveconfiguration::CurveConfiguration, multicurvebootstrapper::MultiCurveBootstrapper,
},
time::{date::Date, daycounter::DayCounter},
utils::errors::Result,
};
struct MapSelector {
reference_date: Date,
quotes: HashMap<String, f64>,
}
impl MapSelector {
fn new(reference_date: Date) -> Self {
Self {
reference_date,
quotes: HashMap::new(),
}
}
fn add(&mut self, id: &str, rate: f64) {
self.quotes.insert(id.to_string(), rate);
}
}
impl QuoteSelector for MapSelector {
fn select(&self, identifier: &str) -> Option<Quote> {
let rate = self.quotes.get(identifier)?;
let det: QuoteDetails = identifier.parse().ok()?;
let q = Quote::new(det, QuoteLevels::with_mid(*rate));
if q.build_instrument(self.reference_date, Level::Mid, None)
.is_ok()
{
Some(q)
} else {
None
}
}
fn reference_date(&self) -> Date {
self.reference_date
}
}
fn rd() -> Date {
Date::new(2024, 6, 1)
}
fn default_policy() -> BootstrapDiscountPolicy {
BootstrapDiscountPolicy::new(MarketIndex::SOFR, Currency::USD)
}
#[test]
fn bootstrap_single_deposit() -> Result<()> {
let mut selector = MapSelector::new(rd());
selector.add("FixedRateDeposit_USD_SOFR_6M", 0.05);
let spec = CurveConfiguration::new(
MarketIndex::SOFR,
DayCounter::Actual360,
Interpolator::LogLinear,
true,
vec!["FixedRateDeposit_USD_SOFR_6M".into()],
);
let bootstrapper = MultiCurveBootstrapper::new(vec![spec], default_policy());
let result = bootstrapper.bootstrap(&selector, Level::Mid)?;
assert!(result.contains_key(&MarketIndex::SOFR));
let curve = result[&MarketIndex::SOFR].curve();
let df = curve.discount_factor(Date::new(2024, 12, 1))?;
assert!(
df.value() > 0.0 && df.value() < 1.0,
"DF should be in (0,1)"
);
Ok(())
}
#[test]
fn bootstrap_deposits_and_swaps() -> Result<()> {
let mut selector = MapSelector::new(rd());
selector.add("FixedRateDeposit_USD_SOFR_3M", 0.05);
selector.add("FixedRateDeposit_USD_SOFR_6M", 0.051);
selector.add("OIS_USD_SOFR_1Y", 0.048);
selector.add("OIS_USD_SOFR_2Y", 0.045);
let spec = CurveConfiguration::new(
MarketIndex::SOFR,
DayCounter::Actual360,
Interpolator::LogLinear,
true,
vec![
"FixedRateDeposit_USD_SOFR_3M".into(),
"FixedRateDeposit_USD_SOFR_6M".into(),
"OIS_USD_SOFR_1Y".into(),
"OIS_USD_SOFR_2Y".into(),
],
);
let bootstrapper = MultiCurveBootstrapper::new(vec![spec], default_policy());
let result = bootstrapper.bootstrap(&selector, Level::Mid)?;
let curve = result[&MarketIndex::SOFR].curve();
let dates = [
Date::new(2024, 9, 1),
Date::new(2024, 12, 1),
Date::new(2025, 6, 1),
Date::new(2026, 6, 1),
];
let mut prev_df = 1.0;
for d in &dates {
let df = curve.discount_factor(*d)?.value();
assert!(df < prev_df, "DF at {d} should be < previous DF");
assert!(df > 0.0, "DF should be positive");
prev_df = df;
}
Ok(())
}
#[test]
fn bootstrap_result_has_ift_sensitivities() -> Result<()> {
let mut selector = MapSelector::new(rd());
selector.add("FixedRateDeposit_USD_SOFR_3M", 0.05);
selector.add("OIS_USD_SOFR_1Y", 0.048);
let spec = CurveConfiguration::new(
MarketIndex::SOFR,
DayCounter::Actual360,
Interpolator::LogLinear,
true,
vec![
"FixedRateDeposit_USD_SOFR_3M".into(),
"OIS_USD_SOFR_1Y".into(),
],
);
let bootstrapper = MultiCurveBootstrapper::new(vec![spec], default_policy());
let result = bootstrapper.bootstrap(&selector, Level::Mid)?;
let elem = &result[&MarketIndex::SOFR];
let curve = elem.curve();
let ift = curve.ift_sensitivities();
assert!(ift.is_some(), "IFT sensitivities should be present");
let sens = ift.unwrap();
assert_eq!(sens.len(), 2, "Should have 2 pillar rows");
assert_eq!(sens[0].len(), 2, "Should have 2 quote columns");
Ok(())
}
#[test]
fn bootstrap_missing_quote_errors() {
let selector = MapSelector::new(rd());
let spec = CurveConfiguration::new(
MarketIndex::SOFR,
DayCounter::Actual360,
Interpolator::LogLinear,
true,
vec!["OIS_USD_SOFR_1Y".into()],
);
let bootstrapper = MultiCurveBootstrapper::new(vec![spec], default_policy());
let result = bootstrapper.bootstrap(&selector, Level::Mid);
assert!(result.is_err());
}
#[test]
fn bootstrap_basis_swap_two_curves() -> Result<()> {
let mut selector = MapSelector::new(rd());
selector.add("FixedRateDeposit_USD_SOFR_6M", 0.05);
selector.add("OIS_USD_SOFR_1Y", 0.048);
selector.add("BasisSwap_USD_SOFR_TermSOFR3m_1Y", 0.001);
let sofr_spec = CurveConfiguration::new(
MarketIndex::SOFR,
DayCounter::Actual360,
Interpolator::LogLinear,
true,
vec![
"FixedRateDeposit_USD_SOFR_6M".into(),
"OIS_USD_SOFR_1Y".into(),
],
);
let term_spec = CurveConfiguration::new(
MarketIndex::TermSOFR3m,
DayCounter::Actual360,
Interpolator::LogLinear,
true,
vec!["BasisSwap_USD_SOFR_TermSOFR3m_1Y".into()],
);
let bootstrapper =
MultiCurveBootstrapper::new(vec![sofr_spec, term_spec], default_policy());
let result = bootstrapper.bootstrap(&selector, Level::Mid)?;
assert!(result.contains_key(&MarketIndex::SOFR));
assert!(result.contains_key(&MarketIndex::TermSOFR3m));
let term_curve = result[&MarketIndex::TermSOFR3m].curve();
let df = term_curve.discount_factor(Date::new(2025, 6, 1))?.value();
assert!(df > 0.0 && df < 1.0);
Ok(())
}
#[test]
fn bootstrap_fx_forward_cross_currency_missing_fx() -> Result<()> {
let mut selector = MapSelector::new(rd());
selector.add("FixedRateDeposit_USD_SOFR_6M", 0.05);
selector.add("OIS_USD_SOFR_1Y", 0.048);
selector.add("FxForwardPoints_EURUSD_6M", 0.005);
selector.add("FxForwardPoints_EURUSD_1Y", 0.008);
let sofr_spec = CurveConfiguration::new(
MarketIndex::SOFR,
DayCounter::Actual360,
Interpolator::LogLinear,
true,
vec![
"FixedRateDeposit_USD_SOFR_6M".into(),
"OIS_USD_SOFR_1Y".into(),
],
);
let collateral_spec = CurveConfiguration::new(
MarketIndex::Collateral(Currency::EUR, Currency::USD),
DayCounter::Actual360,
Interpolator::LogLinear,
true,
vec![
"FxForwardPoints_EURUSD_6M".into(),
"FxForwardPoints_EURUSD_1Y".into(),
],
);
let bootstrapper =
MultiCurveBootstrapper::new(vec![sofr_spec, collateral_spec], default_policy());
assert!(bootstrapper.bootstrap(&selector, Level::Mid).is_err());
Ok(())
}
#[test]
fn bootstrap_fx_forward_cross_currency() -> Result<()> {
let mut selector = MapSelector::new(rd());
selector.add("FixedRateDeposit_USD_SOFR_6M", 0.05);
selector.add("OIS_USD_SOFR_1Y", 0.048);
selector.add("FxForwardPoints_EURUSD_6M", 0.005);
selector.add("FxForwardPoints_EURUSD_1Y", 0.008);
let sofr_spec = CurveConfiguration::new(
MarketIndex::SOFR,
DayCounter::Actual360,
Interpolator::LogLinear,
true,
vec![
"FixedRateDeposit_USD_SOFR_6M".into(),
"OIS_USD_SOFR_1Y".into(),
],
);
let collateral_spec = CurveConfiguration::new(
MarketIndex::Collateral(Currency::EUR, Currency::USD),
DayCounter::Actual360,
Interpolator::LogLinear,
true,
vec![
"FxForwardPoints_EURUSD_6M".into(),
"FxForwardPoints_EURUSD_1Y".into(),
],
);
let mut fx_store = FxStore::new();
fx_store.add_fx_rate(Currency::USD, Currency::EUR, DualFwd::new(1.08));
let bootstrapper =
MultiCurveBootstrapper::new(vec![sofr_spec, collateral_spec], default_policy())
.with_fx_store(fx_store);
let result = bootstrapper.bootstrap(&selector, Level::Mid)?;
assert!(result.contains_key(&MarketIndex::SOFR));
assert!(result.contains_key(&MarketIndex::Collateral(Currency::EUR, Currency::USD)));
let coll_curve = result[&MarketIndex::Collateral(Currency::EUR, Currency::USD)].curve();
let df = coll_curve.discount_factor(Date::new(2025, 6, 1))?.value();
assert!(df > 0.0 && df < 1.5, "Collateral DF should be reasonable");
Ok(())
}
#[test]
fn bootstrap_fx_forward_cross_currency_inverse_parity() -> Result<()> {
let mut selector = MapSelector::new(rd());
selector.add("FixedRateDeposit_USD_SOFR_6M", 0.05);
selector.add("OIS_USD_SOFR_1Y", 0.048);
selector.add("FxForwardPoints_EURUSD_6M", 0.005);
selector.add("FxForwardPoints_EURUSD_1Y", 0.008);
let sofr_spec = CurveConfiguration::new(
MarketIndex::SOFR,
DayCounter::Actual360,
Interpolator::LogLinear,
true,
vec![
"FixedRateDeposit_USD_SOFR_6M".into(),
"OIS_USD_SOFR_1Y".into(),
],
);
let collateral_spec = CurveConfiguration::new(
MarketIndex::Collateral(Currency::EUR, Currency::USD),
DayCounter::Actual360,
Interpolator::LogLinear,
true,
vec![
"FxForwardPoints_EURUSD_6M".into(),
"FxForwardPoints_EURUSD_1Y".into(),
],
);
let mut fx_store = FxStore::new();
fx_store.add_fx_rate(Currency::EUR, Currency::USD, DualFwd::new(1.0/1.08));
fx_store.add_fx_rate(Currency::CLP, Currency::USD, DualFwd::new(900.0));
let bootstrapper =
MultiCurveBootstrapper::new(vec![sofr_spec, collateral_spec], default_policy())
.with_fx_store(fx_store);
let result = bootstrapper.bootstrap(&selector, Level::Mid)?;
assert!(result.contains_key(&MarketIndex::SOFR));
assert!(result.contains_key(&MarketIndex::Collateral(Currency::EUR, Currency::USD)));
let coll_curve = result[&MarketIndex::Collateral(Currency::EUR, Currency::USD)].curve();
let df = coll_curve.discount_factor(Date::new(2025, 6, 1))?.value();
assert!(df > 0.0 && df < 1.5, "Collateral DF should be reasonable");
Ok(())
}
#[test]
fn bootstrap_rate_futures() -> Result<()> {
let mut selector = MapSelector::new(rd());
selector.add("Future_USD_SOFR_U4", 95.0);
selector.add("Future_USD_SOFR_Z4", 95.5);
let spec = CurveConfiguration::new(
MarketIndex::SOFR,
DayCounter::Actual360,
Interpolator::LogLinear,
true,
vec!["Future_USD_SOFR_U4".into(), "Future_USD_SOFR_Z4".into()],
);
let bootstrapper = MultiCurveBootstrapper::new(vec![spec], default_policy());
let result = bootstrapper.bootstrap(&selector, Level::Mid)?;
assert!(result.contains_key(&MarketIndex::SOFR));
let curve = result[&MarketIndex::SOFR].curve();
let df = curve.discount_factor(Date::new(2024, 12, 18))?.value();
assert!(df > 0.0 && df < 1.0);
Ok(())
}
#[test]
fn bootstrap_reprices_inputs() -> Result<()> {
let mut selector = MapSelector::new(rd());
let rates = [
("FixedRateDeposit_USD_SOFR_3M", 0.05),
("FixedRateDeposit_USD_SOFR_6M", 0.051),
("OIS_USD_SOFR_1Y", 0.048),
];
for (id, rate) in &rates {
selector.add(id, *rate);
}
let spec = CurveConfiguration::new(
MarketIndex::SOFR,
DayCounter::Actual360,
Interpolator::LogLinear,
true,
rates.iter().map(|(id, _)| (*id).into()).collect(),
);
let bootstrapper = MultiCurveBootstrapper::new(vec![spec], default_policy());
let result = bootstrapper.bootstrap(&selector, Level::Mid)?;
let elem = &result[&MarketIndex::SOFR];
let curve = elem.curve();
let nodes = curve.nodes();
assert!(nodes.is_some(), "Nodes should be available");
let nodes = nodes.unwrap();
assert_eq!(nodes.len(), 4);
for (_date, df) in &nodes {
assert!(df.value() > 0.0 && df.value() <= 1.0);
}
Ok(())
}
}