quantsupport 0.1.7

Rust quantitative finance library for derivatives pricing, yield-curve bootstrapping, AAD risk, Monte Carlo exposure, and XVA.
Documentation
#[cfg(test)]
use crate::ad::dual::DualFwd;
use crate::{
    ad::scalar::Scalar,
    core::trade::Side,
    currencies::currency::Currency,
    indices::marketindex::MarketIndex,
    instruments::{
        cashflows::makeleg::{MakeLeg, RateType},
        rates::basisswap::BasisSwap,
    },
    time::{
        calendar::Calendar,
        date::Date,
        enums::{BusinessDayConvention, DateGenerationRule, Frequency},
    },
    utils::errors::{QSError, Result},
};
use std::marker::PhantomData;

/// A builder for creating a [`BasisSwap`] instance (floating-vs-floating interest rate swap).
///
/// ## Example
/// ```rust
/// use quantsupport::prelude::*;
///
/// let basis_swap = MakeBasisSwap::<DualFwd>::default()
///     .with_identifier("BASIS-3M6M".to_string())
///     .with_start_date(Date::new(2024, 1, 1))
///     .with_maturity_date(Date::new(2026, 1, 1))
///     .with_notional(5_000_000.0)
///     .with_pay_market_index(MarketIndex::SOFR)
///     .with_receive_market_index(MarketIndex::TermSOFR3m)
///     .with_currency(Currency::USD)
///     .with_pay_spread(0.001)
///     .with_receive_spread(0.0)
///     .build()
///     .expect("failed to build basis swap");
///
/// assert_eq!(basis_swap.identifier(), "BASIS-3M6M");
/// assert!(!basis_swap.pay_leg().cashflows().is_empty());
/// assert!(!basis_swap.receive_leg().cashflows().is_empty());
/// ```
#[derive(Default)]
pub struct MakeBasisSwap<T: Scalar> {
    start_date: Option<Date>,
    maturity_date: Option<Date>,
    notional: Option<f64>,
    pay_spread: Option<f64>,
    receive_spread: Option<f64>,
    identifier: Option<String>,
    pay_market_index: Option<MarketIndex>,
    receive_market_index: Option<MarketIndex>,
    currency: Option<Currency>,
    side: Option<Side>,
    pay_leg_frequency: Option<Frequency>,
    receive_leg_frequency: Option<Frequency>,
    calendar: Option<Calendar>,
    business_day_convention: Option<BusinessDayConvention>,
    date_generation_rule: Option<DateGenerationRule>,
    end_of_month: Option<bool>,
    _marker: PhantomData<T>,
}

impl<T> MakeBasisSwap<T>
where
    T: Scalar,
{
    /// Sets the start date.
    #[must_use]
    pub const fn with_start_date(mut self, date: Date) -> Self {
        self.start_date = Some(date);
        self
    }

    /// Sets the maturity date.
    #[must_use]
    pub const fn with_maturity_date(mut self, date: Date) -> Self {
        self.maturity_date = Some(date);
        self
    }

    /// Sets the notional amount.
    #[must_use]
    pub const fn with_notional(mut self, notional: f64) -> Self {
        self.notional = Some(notional);
        self
    }

    /// Sets the spread on the pay leg.
    #[must_use]
    pub const fn with_pay_spread(mut self, spread: f64) -> Self {
        self.pay_spread = Some(spread);
        self
    }

    /// Sets the spread on the receive leg.
    #[must_use]
    pub const fn with_receive_spread(mut self, spread: f64) -> Self {
        self.receive_spread = Some(spread);
        self
    }

    /// Sets the identifier.
    #[must_use]
    pub fn with_identifier(mut self, identifier: String) -> Self {
        self.identifier = Some(identifier);
        self
    }

    /// Sets the market index for the pay leg.
    #[must_use]
    pub fn with_pay_market_index(mut self, idx: MarketIndex) -> Self {
        self.pay_market_index = Some(idx);
        self
    }

    /// Sets the market index for the receive leg.
    #[must_use]
    pub fn with_receive_market_index(mut self, idx: MarketIndex) -> Self {
        self.receive_market_index = Some(idx);
        self
    }

    /// Sets the currency of the swap.
    #[must_use]
    pub const fn with_currency(mut self, currency: Currency) -> Self {
        self.currency = Some(currency);
        self
    }

    /// Sets the side.
    #[must_use]
    pub const fn with_side(mut self, side: Side) -> Self {
        self.side = Some(side);
        self
    }

    /// Sets the pay leg payment frequency.
    #[must_use]
    pub const fn with_pay_leg_frequency(mut self, freq: Frequency) -> Self {
        self.pay_leg_frequency = Some(freq);
        self
    }

    /// Sets the receive leg payment frequency.
    #[must_use]
    pub const fn with_receive_leg_frequency(mut self, freq: Frequency) -> Self {
        self.receive_leg_frequency = Some(freq);
        self
    }

    /// Sets the calendar for business day adjustments.
    #[must_use]
    pub fn with_calendar(mut self, calendar: Calendar) -> Self {
        self.calendar = Some(calendar);
        self
    }

    /// Sets the business day convention.
    #[must_use]
    pub const fn with_business_day_convention(mut self, convention: BusinessDayConvention) -> Self {
        self.business_day_convention = Some(convention);
        self
    }

    /// Sets the date generation rule.
    #[must_use]
    pub const fn with_date_generation_rule(mut self, rule: DateGenerationRule) -> Self {
        self.date_generation_rule = Some(rule);
        self
    }

    /// Sets the end-of-month flag.
    #[must_use]
    pub const fn with_end_of_month(mut self, eom: bool) -> Self {
        self.end_of_month = Some(eom);
        self
    }

    /// Builds the [`BasisSwap`] instance.
    ///
    /// # Errors
    /// Returns an error if any of the required fields are missing or invalid.
    pub fn build(self) -> Result<BasisSwap<T>> {
        let notional = self
            .notional
            .ok_or_else(|| QSError::ValueNotSetErr("Notional".into()))?;
        let start_date = self
            .start_date
            .ok_or_else(|| QSError::ValueNotSetErr("Start date".into()))?;
        let maturity_date = self
            .maturity_date
            .ok_or_else(|| QSError::ValueNotSetErr("Maturity date".into()))?;
        let currency = self
            .currency
            .ok_or_else(|| QSError::ValueNotSetErr("Currency".into()))?;
        let pay_market_index = self
            .pay_market_index
            .ok_or_else(|| QSError::ValueNotSetErr("Pay market index".into()))?;
        let receive_market_index = self
            .receive_market_index
            .ok_or_else(|| QSError::ValueNotSetErr("Receive market index".into()))?;
        let identifier = self
            .identifier
            .ok_or_else(|| QSError::ValueNotSetErr("Identifier".into()))?;

        let pay_spread = self.pay_spread.unwrap_or(0.0);
        let receive_spread = self.receive_spread.unwrap_or(0.0);
        let pay_freq = self.pay_leg_frequency.unwrap_or(Frequency::Quarterly);
        let receive_freq = self.receive_leg_frequency.unwrap_or(Frequency::Quarterly);

        // Pay leg
        let pay_leg = MakeLeg::<T>::default()
            .with_leg_id(0)
            .with_notional(notional)
            .with_side(Side::PayShort)
            .with_currency(currency)
            .with_forward_index(pay_market_index.clone())
            .with_start_date(start_date)
            .with_end_date(maturity_date)
            .with_rate_type(RateType::Floating)
            .with_spread(pay_spread)
            .with_payment_frequency(pay_freq)
            .bullet()
            .with_calendar(self.calendar.clone())
            .with_business_day_convention(self.business_day_convention)
            .with_date_generation_rule(self.date_generation_rule)
            .with_end_of_month(self.end_of_month)
            .build()?;

        // Receive leg
        let receive_leg = MakeLeg::<T>::default()
            .with_leg_id(1)
            .with_notional(notional)
            .with_side(Side::LongReceive)
            .with_currency(currency)
            .with_forward_index(receive_market_index.clone())
            .with_start_date(start_date)
            .with_end_date(maturity_date)
            .with_rate_type(RateType::Floating)
            .with_spread(receive_spread)
            .with_payment_frequency(receive_freq)
            .bullet()
            .with_calendar(self.calendar)
            .with_business_day_convention(self.business_day_convention)
            .with_date_generation_rule(self.date_generation_rule)
            .with_end_of_month(self.end_of_month)
            .build()?;

        Ok(BasisSwap::new(
            identifier,
            pay_leg,
            receive_leg,
            pay_market_index,
            receive_market_index,
            currency,
        ))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::instrument::Instrument;

    fn base_builder() -> MakeBasisSwap<DualFwd> {
        MakeBasisSwap::<DualFwd>::default()
            .with_identifier("basis_swap_test".to_string())
            .with_start_date(Date::new(2024, 1, 1))
            .with_maturity_date(Date::new(2025, 1, 1))
            .with_notional(1_000_000.0)
            .with_pay_market_index(MarketIndex::SOFR)
            .with_receive_market_index(MarketIndex::TermSOFR3m)
            .with_currency(Currency::USD)
    }

    #[test]
    fn test_build_basis_swap_success() {
        let result = base_builder().build();
        assert!(result.is_ok(), "expected basis swap build to succeed");

        let swap = result.unwrap();
        assert_eq!(swap.identifier(), "basis_swap_test");
        assert_eq!(swap.currency(), Currency::USD);
        assert_eq!(swap.pay_forward_index(), MarketIndex::SOFR);
        assert_eq!(swap.receive_forward_index(), MarketIndex::TermSOFR3m);
        assert!(!swap.pay_leg().cashflows().is_empty());
        assert!(!swap.receive_leg().cashflows().is_empty());
    }

    #[test]
    fn test_build_basis_swap_missing_receive_index_fails() {
        let result = MakeBasisSwap::<DualFwd>::default()
            .with_identifier("basis_swap_missing_receive".to_string())
            .with_start_date(Date::new(2024, 1, 1))
            .with_maturity_date(Date::new(2025, 1, 1))
            .with_notional(1_000_000.0)
            .with_pay_market_index(MarketIndex::SOFR)
            .with_currency(Currency::USD)
            .build();

        assert!(
            result.is_err(),
            "expected missing receive market index to fail"
        );
    }
}