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::handle::Handle;
use crate::indexes::{Index, inflation::UkRpi};
use crate::interestrate::Compounding;
use crate::math::{interpolations::linear::Linear, matrix::Matrix};
use crate::pricingengines::InterpolatingCpiCapFloorEngine;
use crate::shared::{shared, shared_mut};
use crate::termstructures::inflation::cpicapfloortermpricesurface::{
    CpiCapFloorSurfaceConfig, CpiCapFloorTermPriceSurface,
};
use crate::termstructures::inflation::inflationtermstructure::ZeroInflationTermStructure;
use crate::termstructures::inflation::interpolatedzeroinflationcurve::ZeroInflationCurve;
use crate::termstructures::{yields::FlatForward, yieldtermstructure::YieldTermStructure};
use crate::time::{
    calendars::NullCalendar, date::Month, daycounters::actual365fixed::Actual365Fixed,
    frequency::Frequency,
};

fn d(day: i32, month: Month, year: i32) -> Date {
    Date::new(day, month, year)
}
fn rows(values: &[[f64; 2]]) -> Matrix {
    let mut out = Matrix::with_size(values.len(), 2);
    for (i, row) in values.iter().enumerate() {
        out.row_mut(i).copy_from_slice(row);
    }
    out
}
fn market() -> CpiCapFloorSurfaceConfig {
    let today = d(1, Month::September, 2026);
    let settings = shared(Settings::<Date>::new());
    settings.set_evaluation_date(today);
    let dc = Actual365Fixed::new();
    let curve = shared(
        ZeroInflationCurve::new(
            today,
            vec![d(1, Month::July, 2026), d(1, Month::July, 2040)],
            vec![0.03, 0.03],
            Frequency::Monthly,
            dc.clone(),
            Linear,
            None,
        )
        .unwrap(),
    );
    let index = shared(
        UkRpi::new(settings.clone())
            .with_term_structure(Handle::new(curve as Shared<dyn ZeroInflationTermStructure>)),
    );
    index.add_fixing(d(1, Month::June, 2026), 200.0).unwrap();
    index.add_fixing(d(1, Month::July, 2026), 200.0).unwrap();
    let discount = shared(FlatForward::with_rate(
        today,
        0.05,
        dc.clone(),
        Compounding::Continuous,
        Frequency::Annual,
    ));
    CpiCapFloorSurfaceConfig {
        nominal: 1.0,
        base_rate: 0.03,
        observation_lag: Period::new(3, TimeUnit::Months),
        calendar: NullCalendar::new(),
        business_day_convention: BusinessDayConvention::Unadjusted,
        day_counter: dc,
        index,
        interpolation: CpiInterpolationType::Flat,
        discount: Handle::new(discount as Shared<dyn YieldTermStructure>),
        cap_strikes: vec![0.04, 0.06],
        floor_strikes: vec![0.0, 0.02],
        maturities: vec![
            Period::new(1, TimeUnit::Years),
            Period::new(3, TimeUnit::Years),
        ],
        cap_prices: rows(&[[0.03, 0.06], [0.01, 0.02]]),
        floor_prices: rows(&[[0.005, 0.01], [0.02, 0.04]]),
        settings,
    }
}
fn config(
    a: &CpiCapFloorSurfaceConfig,
    kind: OptionType,
    interpolation: CpiInterpolationType,
) -> CpiCapFloorConfig {
    CpiCapFloorConfig {
        option_type: kind,
        nominal: 1e6,
        start_date: d(1, Month::September, 2026),
        base_cpi: 200.0,
        maturity: d(15, Month::September, 2028),
        fixing_calendar: a.calendar.clone(),
        fixing_convention: BusinessDayConvention::Unadjusted,
        payment_calendar: a.calendar.clone(),
        payment_convention: BusinessDayConvention::Unadjusted,
        strike: 0.05,
        index: a.index.clone(),
        observation_lag: a.observation_lag,
        interpolation,
        settings: a.settings.clone(),
    }
}
#[test]
fn cpi_price_surface_grid_interpolation_and_quoted_units() {
    let a = market();
    let settings = a.settings.clone();
    let mut c = config(&a, OptionType::Call, CpiInterpolationType::Flat);
    let surface = shared(CpiCapFloorTermPriceSurface::new(a).unwrap());
    let date = d(1, Month::September, 2027);
    assert!((surface.cap_price(date, 0.04).unwrap() - 0.03).abs() < 1e-14);
    assert!((surface.floor_price(date, 0.02).unwrap() - 0.02).abs() < 1e-14);
    assert!((surface.cap_price(date, 0.05).unwrap() - 0.02).abs() < 1e-14);
    assert!((surface.cap_price(date, 0.08).unwrap() + 0.01).abs() < 1e-14);
    let engine = shared_mut(InterpolatingCpiCapFloorEngine::new(surface.clone()));
    let mut first = CpiCapFloor::new(c.clone()).unwrap();
    first.set_pricing_engine(engine.clone());
    let value = first.npv().unwrap();
    c.nominal = 7.0;
    c.base_cpi = 123.0;
    c.start_date = d(15, Month::July, 2026);
    let mut second = CpiCapFloor::new(c.clone()).unwrap();
    second.set_pricing_engine(engine.clone());
    assert!((value - second.npv().unwrap()).abs() < 1e-14);
    let period_start = d(1, Month::September, 2028);
    assert!((value - surface.cap_price(period_start, c.strike).unwrap()).abs() < 1e-14);
    settings.set_evaluation_date(d(16, Month::September, 2028));
    assert!(first.is_expired().unwrap());
    assert_eq!(first.npv().unwrap(), 0.0);
    assert_eq!(surface.reference_date(), d(1, Month::September, 2026));
    assert!((surface.cap_price(date, 0.04).unwrap() - 0.03).abs() < 1e-14);
}
#[test]
fn cpi_capfloor_linear_lag_and_dates() {
    let a = market();
    let mut c = config(&a, OptionType::Put, CpiInterpolationType::Linear);
    c.observation_lag = Period::new(6, TimeUnit::Months);
    let surface = shared(CpiCapFloorTermPriceSurface::new(a).unwrap());
    let start = d(1, Month::June, 2028);
    let end = d(1, Month::July, 2028);
    let expected = surface.floor_price(start, c.strike).unwrap()
        + (surface.floor_price(end, c.strike).unwrap()
            - surface.floor_price(start, c.strike).unwrap())
            * 14.0
            / 30.0;
    let mut instrument = CpiCapFloor::new(c.clone()).unwrap();
    instrument.set_pricing_engine(shared_mut(InterpolatingCpiCapFloorEngine::new(
        surface.clone(),
    )));
    assert_eq!(instrument.fixing_date().unwrap(), d(15, Month::March, 2028));
    assert_eq!(
        instrument.pay_date().unwrap(),
        d(15, Month::September, 2028)
    );
    assert!((instrument.npv().unwrap() - expected).abs() < 1e-14);
    c.observation_lag = Period::new(2, TimeUnit::Months);
    let mut wrong = CpiCapFloor::new(c).unwrap();
    wrong.set_pricing_engine(shared_mut(InterpolatingCpiCapFloorEngine::new(surface)));
    assert!(wrong.npv().is_err());
}
#[test]
fn cpi_surface_snapshots_parity_inputs() {
    let a = market();
    let index = a.index.clone();
    let settings = a.settings.clone();
    let surface = CpiCapFloorTermPriceSurface::new(a).unwrap();
    let date = d(1, Month::September, 2027);
    let previous = surface.cap_price(date, 0.0).unwrap();
    let curve = shared(
        ZeroInflationCurve::new(
            d(1, Month::September, 2026),
            vec![d(1, Month::July, 2026), d(1, Month::July, 2040)],
            vec![0.09, 0.09],
            Frequency::Monthly,
            Actual365Fixed::new(),
            Linear,
            None,
        )
        .unwrap(),
    );
    let new_index = shared(
        UkRpi::new(settings.clone())
            .with_term_structure(Handle::new(curve as Shared<dyn ZeroInflationTermStructure>)),
    );
    let mut fresh = market();
    fresh.settings = settings;
    fresh.index = new_index;
    let rebuilt = CpiCapFloorTermPriceSurface::new(fresh).unwrap();
    assert!((surface.cap_price(date, 0.0).unwrap() - previous).abs() < 1e-14);
    assert!((rebuilt.cap_price(date, 0.0).unwrap() - previous).abs() > 0.001);
    assert!(
        index
            .term_structure()
            .current_link()
            .unwrap()
            .zero_rate_date(date, false)
            .is_ok()
    );
}
#[test]
fn cpi_capfloor_rejects_invalid_and_boundary_inputs_without_panics() {
    for unit in [TimeUnit::Days, TimeUnit::Weeks] {
        let mut a = market();
        a.observation_lag = Period::new(30, unit);
        assert!(CpiCapFloorTermPriceSurface::new(a).is_err());
    }
    for y in [0, -1, i32::MAX] {
        let mut a = market();
        a.maturities[1] = Period::new(y, TimeUnit::Years);
        assert!(CpiCapFloorTermPriceSurface::new(a).is_err());
    }
    for k in [f64::NAN, f64::INFINITY, -1.0, f64::MAX] {
        let mut a = market();
        a.cap_strikes[1] = k;
        assert!(CpiCapFloorTermPriceSurface::new(a).is_err());
    }
    let mut a = market();
    a.cap_prices[(0, 0)] = 0.0;
    assert!(CpiCapFloorTermPriceSurface::new(a).is_err());
    let a = market();
    let mut c = config(&a, OptionType::Call, CpiInterpolationType::Flat);
    c.observation_lag = Period::new(i32::MAX, TimeUnit::Years);
    assert!(CpiCapFloor::new(c).is_err());
    let a = market();
    let surface = CpiCapFloorTermPriceSurface::new(a).unwrap();
    assert!(surface.cap_price(Date::null(), 0.04).is_err());
    assert!(
        surface
            .cap_price(d(1, Month::September, 2027), f64::NAN)
            .is_err()
    );
}