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::cashflow::CashFlow;
use crate::cashflows::{Coupon, CpiLeg};
use crate::indexes::Index;
use crate::indexes::inflation::UkRpi;
use crate::settings::Settings;
use crate::shared::shared;
use crate::time::{
    date::Month::*, daycounters::actual365fixed::Actual365Fixed, schedule::Schedule,
    timeunit::TimeUnit,
};

fn index() -> Shared<ZeroInflationIndex> {
    let settings = shared(Settings::new());
    settings.set_evaluation_date(Date::new(15, June, 2025));
    let index = shared(UkRpi::new(settings));
    for (month, year, value) in [
        (October, 2024, 99.5),
        (November, 2024, 99.8),
        (December, 2024, 100.0),
        (January, 2025, 100.2),
        (February, 2025, 100.4),
        (March, 2025, 100.6),
        (April, 2025, 100.8),
        (May, 2025, 101.0),
    ] {
        index.add_fixing(Date::new(1, month, year), value).unwrap();
    }
    index
}
fn coupon(index: Shared<ZeroInflationIndex>, interpolation: CpiInterpolationType) -> CpiCoupon {
    let coupon = CpiCoupon::new(
        100.0,
        Date::new(15, July, 2025),
        100000.0,
        Date::new(15, January, 2025),
        Date::new(15, July, 2025),
        index,
        Period::new(3, TimeUnit::Months),
        interpolation,
        Actual365Fixed::new(),
        0.02,
        None,
        None,
        Some(Date::new(10, July, 2025)),
    )
    .unwrap();
    coupon.set_pricer(shared(CpiCouponPricer::default()));
    coupon
}
fn near(actual: f64, expected: f64) {
    assert!((actual - expected).abs() < 1e-10, "{actual} != {expected}");
}
#[test]
fn explicit_base_coupon_matches_independent_quantlib_flat_and_linear() {
    let index = index();
    let flat = coupon(index.clone(), CpiInterpolationType::Flat);
    near(flat.index_fixing().unwrap(), 100.8);
    near(flat.rate().unwrap(), 0.02016);
    near(Coupon::amount(&flat).unwrap(), 999.7150684931507);
    near(
        flat.accrued_amount(Date::new(15, June, 2025)).unwrap(),
        832.3616438356165,
    );
    near(
        flat.accrued_amount(Date::new(10, July, 2025)).unwrap(),
        -27.61643835616438,
    );
    let linear = coupon(index.clone(), CpiInterpolationType::Linear);
    near(linear.index_fixing().unwrap(), 100.8 + 0.2 * 14.0 / 31.0);
    assert_eq!(flat.accrued_amount(Date::new(16, July, 2025)).unwrap(), 0.0);
    Index::settings(&*index).clear_fixing(&index.name());
    index.add_fixing(Date::new(1, April, 2025), 101.8).unwrap();
    index.add_fixing(Date::new(1, May, 2025), 101.0).unwrap();
    near(flat.index_fixing().unwrap(), 101.8);
    near(linear.index_fixing().unwrap(), 101.8 - 0.8 * 14.0 / 31.0);
}
#[test]
fn cpi_principal_observes_contract_end_not_payment_and_growth_switch() {
    let index = index();
    for growth in [false, true] {
        let flow = CpiCashFlow::new(
            100000.0,
            index.clone(),
            100.0,
            Date::new(15, July, 2025),
            Period::new(3, TimeUnit::Months),
            CpiInterpolationType::Flat,
            Date::new(15, August, 2025),
            growth,
        )
        .unwrap();
        near(
            flow.amount().unwrap(),
            if growth { 800.0 } else { 100800.0 },
        );
        Index::settings(&*index).set_evaluation_date(Date::new(15, January, 2026));
        let missing = CpiCashFlow::new(
            100000.0,
            index.clone(),
            100.0,
            Date::new(15, January, 2026),
            Period::new(3, TimeUnit::Months),
            CpiInterpolationType::Flat,
            Date::new(15, January, 2026),
            growth,
        )
        .unwrap();
        assert!(missing.amount().is_err());
    }
}
#[test]
fn zero_rate_leg_coupon_needs_no_fixing_but_principal_does() {
    let settings = shared(Settings::new());
    settings.set_evaluation_date(Date::new(15, January, 2026));
    let index = shared(UkRpi::new(settings));
    let schedule = Schedule::from_dates(vec![
        Date::new(15, January, 2025),
        Date::new(15, July, 2025),
    ]);
    let flows = CpiLeg::new(schedule, index, 100.0, Period::new(3, TimeUnit::Months))
        .with_notional(100000.0)
        .with_fixed_rate(0.0)
        .build()
        .unwrap();
    assert_eq!(flows[0].amount().unwrap(), 0.0);
    assert!(flows[1].amount().is_err());
}
#[test]
fn explicit_base_and_date_guards_are_fallible() {
    let index = index();
    for base in [0.0, 1e-17, f64::NAN, f64::INFINITY] {
        assert!(
            CpiCashFlow::new(
                1.0,
                index.clone(),
                base,
                Date::new(1, July, 2025),
                Period::new(3, TimeUnit::Months),
                CpiInterpolationType::Flat,
                Date::new(1, July, 2025),
                false
            )
            .is_err()
        );
    }
    let c = coupon(index.clone(), CpiInterpolationType::Flat);
    assert!(c.index_ratio(Date::null()).is_err());
    assert!(
        checked_cpi_shift(Date::new(1, January, 1901), Period::new(1, TimeUnit::Days)).is_err()
    );
    assert!(
        checked_cpi_shift(
            Date::new(1, January, 2025),
            Period::new(i32::MIN, TimeUnit::Months)
        )
        .is_err()
    );
    assert!(
        checked_cpi_shift(
            Date::new(1, January, 2025),
            Period::new(i32::MAX, TimeUnit::Years)
        )
        .is_err()
    );
    Index::settings(&*index).set_evaluation_date(Date::new(1, January, 1901));
    assert!(c.rate().is_err());
}

#[test]
fn preceding_payment_before_accrual_end_and_zero_ex_period_are_valid() {
    use crate::time::{
        businessdayconvention::BusinessDayConvention as Bdc, calendars::WeekendsOnly,
    };
    let schedule = Schedule::from_dates(vec![
        Date::new(19, January, 2025),
        Date::new(19, July, 2025),
    ]);
    let leg = CpiLeg::new(schedule, index(), 100.0, Period::new(3, TimeUnit::Months))
        .with_notional(1000.0)
        .with_fixed_rate(0.02)
        .with_payment_calendar(WeekendsOnly::new())
        .with_payment_adjustment(Bdc::Preceding)
        .with_ex_coupon_period(
            Period::new(0, TimeUnit::Days),
            WeekendsOnly::new(),
            Bdc::Preceding,
            false,
        )
        .build()
        .unwrap();
    assert_eq!(leg[0].date(), Date::new(18, July, 2025));
    assert!(leg[0].amount().is_ok());
    assert_eq!(leg[0].ex_coupon_date(), None);
}
#[test]
fn safe_calendar_walk_and_linear_boundary_are_fallible() {
    use super::super::cpidates::checked_cpi_adjust;
    use crate::time::{
        businessdayconvention::BusinessDayConvention as Bdc, calendars::WeekendsOnly,
    };
    let calendar = WeekendsOnly::new();
    calendar.add_holiday(Date::min_date());
    assert!(checked_cpi_adjust(&calendar, Date::min_date(), Bdc::Preceding).is_err());
    calendar.add_holiday(Date::max_date());
    assert!(checked_cpi_adjust(&calendar, Date::max_date(), Bdc::Following).is_err());
    let i = index();
    assert!(
        observed_cpi(
            &i,
            Date::max_date(),
            Period::new(0, TimeUnit::Days),
            CpiInterpolationType::Linear
        )
        .is_err()
    );
}

#[test]
fn irregular_reference_period_after_2099_remains_supported() {
    use crate::time::{
        businessdayconvention::BusinessDayConvention as Bdc, calendars::NullCalendar,
    };
    let schedule = Schedule::with_metadata(
        vec![Date::new(15, January, 2101), Date::new(15, April, 2101)],
        NullCalendar::new(),
        Bdc::Unadjusted,
        None,
        Some(Period::new(6, TimeUnit::Months)),
        None,
        None,
        vec![false],
    );
    let leg = CpiLeg::new(schedule, index(), 100.0, Period::new(3, TimeUnit::Months))
        .with_notional(100.0)
        .with_fixed_rate(0.02)
        .build()
        .unwrap();
    assert_eq!(
        leg[0].as_coupon().unwrap().reference_period_end(),
        Date::new(15, July, 2101)
    );
}