libitofin 0.52.1

A ground-up Rust port of QuantLib: quantitative-finance primitives for pricing, risk, and numerical methods.
Documentation
use super::*;
use crate::indexes::ibor::euribor::Euribor;
use crate::interestrate::Compounding;
use crate::quotes::SimpleQuote;
use crate::settings::Settings;
use crate::termstructures::yields::{DepositRateHelper, FlatForward};
use crate::time::{
    businessdayconvention::BusinessDayConvention, calendars::target::Target, date::Month,
    daycounters::actual360::Actual360, frequency::Frequency, timeunit::TimeUnit,
};

struct Fixture {
    settings: Shared<Settings<Date>>,
    reference: Date,
    base: Shared<IborIndex>,
    helper: Shared<dyn RateHelper>,
    basis: Vec<BasisSwapHelperConfig>,
    rate: Shared<SimpleQuote>,
}

impl Fixture {
    fn new() -> Self {
        let reference = Date::new(23, Month::October, 2025);
        let settings = shared(Settings::<Date>::new());
        settings.set_evaluation_date(reference);
        let base = shared(Euribor::three_months(Handle::empty(), settings.clone()));
        let other = shared(Euribor::six_months(Handle::empty(), settings.clone()));
        let rate = shared(SimpleQuote::new(0.03));
        let helper =
            DepositRateHelper::new(Handle::new(rate.clone()), &base) as Shared<dyn RateHelper>;
        let discount = Handle::new(shared(FlatForward::with_rate(
            reference,
            0.02,
            Actual360::new(),
            Compounding::Continuous,
            Frequency::Annual,
        )) as Shared<dyn YieldTermStructure>);
        let template = BasisSwapHelperConfig {
            quote: Handle::new(shared(SimpleQuote::new(0.002))),
            tenor: Period::new(2, TimeUnit::Years),
            settlement_days: 2,
            calendar: Target::new(),
            convention: BusinessDayConvention::ModifiedFollowing,
            end_of_month: false,
            base_index: base.clone(),
            other_index: other,
            discount,
            bootstrap_base_curve: true,
        };
        let mut other_short = template.clone();
        other_short.tenor = Period::new(6, TimeUnit::Months);
        other_short.bootstrap_base_curve = false;
        let mut other_long = other_short.clone();
        other_long.tenor = Period::new(2, TimeUnit::Years);
        Self {
            settings,
            reference,
            base,
            helper,
            basis: vec![template, other_short, other_long],
            rate,
        }
    }

    fn build(&self) -> QlResult<JointYieldCurves> {
        JointYieldCurves::new(
            self.reference,
            [vec![self.helper.clone()], vec![]],
            &self.basis,
            Actual360::new(),
            1e-12,
        )
    }
}

#[test]
fn exported_index_retains_joint_owner_until_its_last_consumer_drops() {
    let fixture = Fixture::new();
    let joint = fixture.build().unwrap();
    let first = joint.curve(0).unwrap();
    let second = joint.curve(1).unwrap();
    let weak_first = Shared::downgrade(&first.current_link().unwrap());
    let weak_second = Shared::downgrade(&second.current_link().unwrap());
    let consumer = fixture.base.clone_with(first.clone());
    let value = first.current_link().unwrap().discount(1.0, true).unwrap();
    drop(joint);
    drop(first);
    drop(second);
    fixture.rate.set_value(0.031);
    assert!(weak_first.upgrade().is_some());
    assert!(weak_second.upgrade().is_some());
    let forecast = consumer.forwarding_term_structure().clone();
    let updated = forecast
        .current_link()
        .unwrap()
        .discount(1.0, true)
        .unwrap();
    assert!((updated - value).abs() > 1e-8);
    drop(consumer);
    assert!(weak_first.upgrade().is_some());
    drop(forecast);
    assert!(weak_first.upgrade().is_none());
    assert!(weak_second.upgrade().is_none());
}

#[test]
fn joint_rejects_helper_membership_before_an_existing_curve_is_queried() {
    let fixture = Fixture::new();
    let old = PiecewiseYieldCurve::<Discount, LogLinear>::new(
        fixture.reference,
        vec![fixture.helper.clone()],
        Actual360::new(),
        LogLinear,
    )
    .unwrap();
    assert!(
        fixture
            .build()
            .err()
            .unwrap()
            .to_string()
            .contains("belongs")
    );
    assert!(old.discount(0.1, true).unwrap().is_finite());
    assert!(fixture.build().is_err());
    drop(old);
    assert!(fixture.build().is_ok());
}

#[test]
fn joint_rejects_unqueried_additional_helper_membership() {
    let fixture = Fixture::new();
    let primary = DepositRateHelper::from_rate(0.031, &fixture.base) as Shared<dyn RateHelper>;
    let old = Curve::with_bootstrap(
        fixture.reference,
        vec![primary],
        Actual360::new(),
        LogLinear,
        GlobalBootstrap::with_penalties(
            vec![fixture.helper.clone()],
            None,
            Some(1e-12),
            None,
            vec![],
            |_, _| vec![],
        ),
    )
    .unwrap();
    assert!(fixture.build().is_err());
    drop(old);
    assert!(fixture.build().is_ok());
}

#[test]
fn invalid_joint_inputs_leave_helpers_reusable() {
    let mut fixture = Fixture::new();
    assert!(
        JointYieldCurves::new(
            Date::null(),
            [vec![fixture.helper.clone()], vec![]],
            &fixture.basis,
            Actual360::new(),
            1e-12
        )
        .is_err()
    );
    let saved = fixture.basis.clone();
    fixture.basis[2].tenor = Period::new(-2, TimeUnit::Years);
    assert!(fixture.build().is_err());
    fixture.basis = saved;
    assert!(
        JointYieldCurves::new(
            fixture.reference,
            [vec![fixture.helper.clone()], vec![fixture.helper.clone()]],
            &fixture.basis,
            Actual360::new(),
            1e-12
        )
        .is_err()
    );
    let joint = fixture.build().unwrap();
    assert!(joint.curve(2).is_err());
    assert!(fixture.build().is_err());
    assert!(
        joint
            .curve(0)
            .unwrap()
            .current_link()
            .unwrap()
            .discount(1.0, true)
            .unwrap()
            .is_finite()
    );
    drop(joint);
    assert!(fixture.build().is_ok());
}

#[test]
fn joint_requires_compatible_settings_and_both_bootstrap_sides() {
    let mut fixture = Fixture::new();
    fixture
        .basis
        .iter_mut()
        .for_each(|item| item.bootstrap_base_curve = true);
    assert!(fixture.build().is_err());
    fixture.basis[2].bootstrap_base_curve = false;
    let settings = shared(Settings::<Date>::new());
    settings.set_evaluation_date(fixture.reference);
    let foreign = shared(Euribor::six_months(Handle::empty(), settings));
    fixture
        .basis
        .iter_mut()
        .for_each(|item| item.other_index = foreign.clone());
    assert!(fixture.build().is_err());
    assert_eq!(fixture.settings.evaluation_date(), Some(fixture.reference));
}

#[test]
fn legacy_piecewise_reuse_stays_available_and_all_live_owners_are_recorded() {
    let fixture = Fixture::new();
    let make = || {
        PiecewiseYieldCurve::<Discount, LogLinear>::new(
            fixture.reference,
            vec![fixture.helper.clone()],
            Actual360::new(),
            LogLinear,
        )
        .unwrap()
    };
    let first = make();
    let second = make();
    assert!(fixture.build().is_err());
    drop(second);
    assert!(fixture.build().is_err());
    drop(first);
    assert!(fixture.build().is_ok());
}