libitofin 0.52.1

A ground-up Rust port of QuantLib: quantitative-finance primitives for pricing, risk, and numerical methods.
Documentation
//! Overnight-Ibor basis helper from v1.44 `basisswapratehelpers.cpp:156-302`.

use std::cell::RefCell;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::rc::Weak;

use crate::cashflow::CashFlow;
use crate::cashflows::{IborLeg, OvernightLeg};
use crate::errors::{QlError, QlResult};
use crate::event::Event;
use crate::handle::{Handle, RelinkableHandle};
use crate::indexes::iborindex::{IborIndex, OvernightIndex};
use crate::indexes::index::Index;
use crate::indexes::interestrateindex::InterestRateIndex;
use crate::instrument::Instrument;
use crate::instruments::Swap;
use crate::patterns::observable::{AsObservable, Observable};
use crate::pricingengine::PricingEngine;
use crate::pricingengines::swap::DiscountingSwapEngine;
use crate::quotes::Quote;
use crate::settings::Settings;
use crate::shared::{Shared, SharedMut, shared, shared_mut};
use crate::termstructures::bootstraphelper::{
    BootstrapHelperBase, RateHelper, RelativeDateRateHelper,
};
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendar::Calendar;
use crate::time::date::Date;
use crate::time::period::Period;
use crate::time::schedule::MakeSchedule;
use crate::time::timeunit::TimeUnit;
use crate::types::{Integer, Natural, Real};

/// Fits the Ibor forecast curve to a spread paid on a compounded overnight leg.
///
/// The overnight index keeps its supplied forecast curve. An empty discount
/// handle uses the fitted Ibor curve, matching the upstream implementation
/// (the upstream header describes a different fallback).
///
/// Both legs run backward from the unadjusted `spot + tenor` (v1.44 default
/// `DateGeneration::Backward`, `basisswapratehelpers.hpp:147`). Deferred
/// (#1423): the v1.44 `bootstrapBaseCurve`, `paymentLag`, overnight payment
/// frequency, `useIndexedCoupons`, `rule`, averaging, `telescopicValueDates`,
/// `basisOnIborLeg` and stub-index arguments; the helper always uses their
/// defaults.
pub struct OvernightIborBasisSwapRateHelper {
    base: BootstrapHelperBase,
    swap: RefCell<Option<Swap>>,
    date_error: RefCell<Option<QlError>>,
    tenor: Period,
    settlement_days: Natural,
    calendar: Calendar,
    convention: BusinessDayConvention,
    end_of_month: bool,
    base_index: Shared<OvernightIndex>,
    other_index: Shared<IborIndex>,
    discount_handle: Handle<dyn YieldTermStructure>,
    settings: Shared<Settings<Date>>,
    term_structure_handle: RelinkableHandle<dyn YieldTermStructure>,
}

impl OvernightIborBasisSwapRateHelper {
    /// Builds a spot-starting helper with both legs on the Ibor payment schedule.
    ///
    /// # Errors
    /// Returns an error for missing quotes, mismatched settings, unsupported
    /// periods, or an invalid evaluation date or schedule.
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        basis: Handle<dyn Quote>,
        tenor: Period,
        settlement_days: Natural,
        calendar: Calendar,
        convention: BusinessDayConvention,
        end_of_month: bool,
        base_index: &Shared<OvernightIndex>,
        other_index: &Shared<IborIndex>,
        discount_handle: Handle<dyn YieldTermStructure>,
    ) -> QlResult<Shared<Self>> {
        crate::require!(!basis.is_empty(), "basis quote handle is empty");
        crate::require!(
            Shared::ptr_eq(base_index.base().settings(), other_index.base().settings()),
            "basis indices must share settings"
        );
        let span = i64::from(Date::max_date() - Date::min_date());
        crate::require!(
            i64::from(settlement_days) <= span,
            "settlement days exceed supported range"
        );
        crate::require!(
            i64::from(base_index.fixing_days()) <= span
                && i64::from(other_index.fixing_days()) <= span,
            "index fixing days exceed supported range"
        );
        for period in [tenor, other_index.tenor()] {
            let days = match period.units() {
                TimeUnit::Days => 1,
                TimeUnit::Weeks => 7,
                TimeUnit::Months => 28,
                TimeUnit::Years => 365,
                _ => crate::fail!("basis periods must use days, weeks, months or years"),
            };
            crate::require!(
                period.length() > 0 && i64::from(period.length()) * days <= span,
                "basis periods must be positive and within supported range"
            );
        }
        let settings = base_index.base().settings().clone();
        let helper = Shared::new_cyclic(|weak: &Weak<Self>| {
            let weak = weak.clone();
            let on_eval_change = Box::new(move || {
                if let Some(helper) = weak.upgrade() {
                    helper.initialize_dates();
                }
            });
            let term_structure_handle = RelinkableHandle::<dyn YieldTermStructure>::empty();
            let other_index = shared(other_index.clone_with(term_structure_handle.handle()));
            let base = BootstrapHelperBase::new_relative(
                basis,
                Shared::clone(&settings),
                true,
                on_eval_change,
            );
            other_index
                .settings()
                .register_fixing_observer(&other_index.name(), &base.observer());
            base_index.observable().register_observer(&base.observer());
            discount_handle.register_observer(&base.observer());
            Self {
                base,
                swap: RefCell::new(None),
                date_error: RefCell::new(None),
                tenor,
                settlement_days,
                calendar,
                convention,
                end_of_month,
                base_index: Shared::clone(base_index),
                other_index,
                discount_handle,
                settings,
                term_structure_handle,
            }
        });
        helper.try_initialize_dates()?;
        Ok(helper)
    }
}

impl AsObservable for OvernightIborBasisSwapRateHelper {
    fn observable(&self) -> &Observable {
        self.base.observable()
    }
}

impl RateHelper for OvernightIborBasisSwapRateHelper {
    fn base(&self) -> &BootstrapHelperBase {
        &self.base
    }

    /// The curve is brought up to date before the swap is borrowed, so a
    /// stale curve re-bootstraps without re-entering this helper's borrowed
    /// swap, as C++ `LazyObject::calculate` allows (`lazyobject.hpp:257-258`).
    fn implied_quote(&self) -> QlResult<Real> {
        if let Some(error) = self.date_error.borrow().as_ref() {
            return Err(error.clone());
        }
        let curve = self.base.term_structure()?;
        curve.discount(0.0, true)?;
        let mut guard = self.swap.borrow_mut();
        let swap = guard.as_mut().expect("initialize_dates populates the swap");
        swap.recalculate()?;
        Ok(-(swap.npv()? / swap.leg_bps(0)?) * 1.0e-4)
    }

    fn set_term_structure(&self, term_structure: &Shared<dyn YieldTermStructure>) {
        self.term_structure_handle
            .link_to_weak(Shared::downgrade(term_structure));
        self.base.set_term_structure(term_structure);
    }
}

impl OvernightIborBasisSwapRateHelper {
    /// Rebuilds atomically, retaining date failures until a successful update.
    ///
    /// Legacy date/schedule assertions are contained before cached state changes.
    pub fn try_initialize_dates(&self) -> QlResult<()> {
        let result = catch_unwind(AssertUnwindSafe(|| self.rebuild_dates())).unwrap_or_else(|_| {
            Err(QlError::new(
                "invalid overnight basis schedule or date range",
                file!(),
                line!(),
            ))
        });
        match result {
            Ok((swap, earliest, maturity, latest_relevant)) => {
                *self.swap.borrow_mut() = Some(swap);
                self.base.set_earliest_date(earliest);
                self.base.set_maturity_date(maturity);
                self.base.set_latest_relevant_date(latest_relevant);
                self.base.set_pillar_date(latest_relevant);
                self.base.set_latest_date(latest_relevant);
                *self.date_error.borrow_mut() = None;
                Ok(())
            }
            Err(error) => {
                *self.date_error.borrow_mut() = Some(error.clone());
                Err(error)
            }
        }
    }

    /// Rebuilds the swap off the current evaluation date (`initializeDates`,
    /// `.cpp:214-268`). The maturity is the schedule end; the latest relevant
    /// date, which is also the pillar, is the latest of the maturity, both
    /// legs' last payment dates, the overnight index's maturity off its last
    /// fixing and the last Ibor fixing end date (`.cpp:258-267`).
    fn rebuild_dates(&self) -> QlResult<(Swap, Date, Date, Date)> {
        let today = self
            .base
            .evaluation_date()
            .ok_or_else(|| QlError::new("evaluation date is not set", file!(), line!()))?;
        crate::require!(
            today >= Date::min_date() && today <= Date::max_date(),
            "evaluation date outside supported range"
        );
        let earliest = self.calendar.advance(
            today,
            self.settlement_days as Integer,
            TimeUnit::Days,
            BusinessDayConvention::Following,
            false,
        );
        let termination = earliest + self.tenor;
        let schedule = MakeSchedule::new()
            .from(earliest)
            .to(termination)
            .with_tenor(self.other_index.tenor())
            .with_calendar(self.calendar.clone())
            .with_convention(self.convention)
            .end_of_month(self.end_of_month)
            .backwards()
            .build();
        let maturity = schedule.end_date();
        let base_coupons = OvernightLeg::new(schedule.clone(), Shared::clone(&self.base_index))
            .with_notional(100.0)
            .coupons()?;
        let last_base = base_coupons
            .last()
            .ok_or_else(|| QlError::new("overnight leg is empty", file!(), line!()))?;
        let last_base_fixing_end = self
            .base_index
            .maturity_date(self.base_index.value_date(last_base.fixing_date())?)?;
        let last_base_payment = last_base.date();
        let coupons = IborLeg::new(schedule, Shared::clone(&self.other_index))
            .with_notional(100.0)
            .coupons()?;
        let last_other = coupons
            .last()
            .ok_or_else(|| QlError::new("Ibor leg is empty", file!(), line!()))?;
        let latest_relevant = maturity
            .max(last_base_payment.max(last_other.date()))
            .max(last_base_fixing_end.max(last_other.fixing_end_date()?));
        let base_leg = base_coupons
            .into_iter()
            .map(|coupon| coupon as Shared<dyn CashFlow>)
            .collect();
        let other_leg = coupons
            .into_iter()
            .map(|coupon| coupon as Shared<dyn CashFlow>)
            .collect();
        let mut swap = Swap::two_leg(base_leg, other_leg, Shared::clone(&self.settings));
        let discount = if self.discount_handle.is_empty() {
            self.term_structure_handle.handle()
        } else {
            self.discount_handle.clone()
        };
        let engine = shared_mut(DiscountingSwapEngine::new(
            discount,
            None,
            None,
            None,
            Shared::clone(&self.settings),
        ));
        swap.base_mut()
            .set_pricing_engine(engine as SharedMut<dyn PricingEngine>);
        Ok((swap, earliest, maturity, latest_relevant))
    }
}

impl RelativeDateRateHelper for OvernightIborBasisSwapRateHelper {
    fn initialize_dates(&self) {
        let _ = self.try_initialize_dates();
    }
}

#[cfg(test)]
#[path = "overnightbasisswapratehelper_tests.rs"]
mod tests;