use crate::{
ad::scalar::Scalar,
currencies::currency::Currency,
indices::marketindex::MarketIndex,
instruments::rates::{
makeswap::MakeSwap,
europeanswaption::{EuropeanSwaption, SwaptionType},
},
rates::interestrate::RateDefinition,
time::{
calendar::Calendar,
date::Date,
enums::{BusinessDayConvention, DateGenerationRule, Frequency},
},
utils::errors::{QSError, Result},
};
use std::marker::PhantomData;
#[derive(Default)]
pub struct MakeSwaption<T: Scalar + Default> {
start_date: Option<Date>,
swap_tenor_date: Option<Date>,
expiry: Option<Date>,
strike: Option<f64>,
notional: Option<f64>,
identifier: Option<String>,
rate_definition: Option<RateDefinition>,
market_index: Option<MarketIndex>,
currency: Option<Currency>,
swaption_type: Option<SwaptionType>,
fixed_leg_frequency: Option<Frequency>,
floating_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> MakeSwaption<T>
where
T: Scalar + Default,
{
#[must_use]
pub const fn with_start_date(mut self, start_date: Date) -> Self {
self.start_date = Some(start_date);
self
}
#[must_use]
pub const fn with_swap_tenor_date(mut self, date: Date) -> Self {
self.swap_tenor_date = Some(date);
self
}
#[must_use]
pub const fn with_expiry(mut self, expiry: Date) -> Self {
self.expiry = Some(expiry);
self
}
#[must_use]
pub const fn with_strike(mut self, strike: f64) -> Self {
self.strike = Some(strike);
self
}
#[must_use]
pub const fn with_notional(mut self, notional: f64) -> Self {
self.notional = Some(notional);
self
}
#[must_use]
pub fn with_identifier(mut self, identifier: String) -> Self {
self.identifier = Some(identifier);
self
}
#[must_use]
pub const fn with_rate_definition(mut self, rate_definition: RateDefinition) -> Self {
self.rate_definition = Some(rate_definition);
self
}
#[must_use]
pub fn with_market_index(mut self, market_index: MarketIndex) -> Self {
self.market_index = Some(market_index);
self
}
#[must_use]
pub const fn with_currency(mut self, currency: Currency) -> Self {
self.currency = Some(currency);
self
}
#[must_use]
pub const fn with_swaption_type(mut self, swaption_type: SwaptionType) -> Self {
self.swaption_type = Some(swaption_type);
self
}
#[must_use]
pub const fn with_fixed_leg_frequency(mut self, frequency: Frequency) -> Self {
self.fixed_leg_frequency = Some(frequency);
self
}
#[must_use]
pub const fn with_floating_leg_frequency(mut self, frequency: Frequency) -> Self {
self.floating_leg_frequency = Some(frequency);
self
}
#[must_use]
pub fn with_calendar(mut self, calendar: Calendar) -> Self {
self.calendar = Some(calendar);
self
}
#[must_use]
pub const fn with_business_day_convention(mut self, convention: BusinessDayConvention) -> Self {
self.business_day_convention = Some(convention);
self
}
#[must_use]
pub const fn with_date_generation_rule(mut self, rule: DateGenerationRule) -> Self {
self.date_generation_rule = Some(rule);
self
}
#[must_use]
pub const fn with_end_of_month(mut self, eom: bool) -> Self {
self.end_of_month = Some(eom);
self
}
pub fn build(self) -> Result<EuropeanSwaption<T>> {
let strike = self
.strike
.ok_or_else(|| QSError::ValueNotSetErr("Strike".into()))?;
let expiry = self
.expiry
.ok_or_else(|| QSError::ValueNotSetErr("Expiry".into()))?;
let identifier = self
.identifier
.clone()
.ok_or_else(|| QSError::ValueNotSetErr("Identifier".into()))?;
let market_index = self
.market_index
.clone()
.ok_or_else(|| QSError::ValueNotSetErr("Market index".into()))?;
let currency = self
.currency
.ok_or_else(|| QSError::ValueNotSetErr("Currency".into()))?;
let swaption_type = self.swaption_type.unwrap_or(SwaptionType::Payer);
let swap_start = self.start_date.unwrap_or(expiry);
let swap_maturity = self
.swap_tenor_date
.ok_or_else(|| QSError::ValueNotSetErr("Swap tenor date".into()))?;
let mut swap_builder = MakeSwap::<T>::default()
.with_identifier(format!("{identifier}_underlying"))
.with_start_date(swap_start)
.with_maturity_date(swap_maturity)
.with_fixed_rate(strike)
.with_notional(
self.notional
.ok_or_else(|| QSError::ValueNotSetErr("Notional".into()))?,
)
.with_market_index(market_index.clone())
.with_currency(currency);
if let Some(rd) = self.rate_definition {
swap_builder = swap_builder.with_rate_definition(rd);
}
if let Some(f) = self.fixed_leg_frequency {
swap_builder = swap_builder.with_fixed_leg_frequency(f);
}
if let Some(f) = self.floating_leg_frequency {
swap_builder = swap_builder.with_floating_leg_frequency(f);
}
if let Some(c) = self.calendar {
swap_builder = swap_builder.with_calendar(c);
}
if let Some(bdc) = self.business_day_convention {
swap_builder = swap_builder.with_business_day_convention(bdc);
}
if let Some(dgr) = self.date_generation_rule {
swap_builder = swap_builder.with_date_generation_rule(dgr);
}
if let Some(eom) = self.end_of_month {
swap_builder = swap_builder.with_end_of_month(eom);
}
let underlying = swap_builder.build()?;
Ok(EuropeanSwaption::new(
identifier,
underlying,
expiry,
swaption_type,
strike,
market_index,
currency,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
ad::dual::DualFwd,
core::instrument::Instrument,
rates::compounding::Compounding,
time::{daycounter::DayCounter, enums::Frequency},
};
fn sample_rate_definition() -> RateDefinition {
RateDefinition::new(
DayCounter::Actual360,
Compounding::Simple,
Frequency::Semiannual,
)
}
fn base_builder() -> MakeSwaption<DualFwd> {
MakeSwaption::<DualFwd>::default()
.with_identifier("swaption_test".to_string())
.with_expiry(Date::new(2024, 6, 1))
.with_swap_tenor_date(Date::new(2026, 6, 1))
.with_strike(0.03)
.with_notional(1_000_000.0)
.with_rate_definition(sample_rate_definition())
.with_market_index(MarketIndex::SOFR)
.with_currency(Currency::USD)
.with_swaption_type(SwaptionType::Payer)
}
#[test]
fn test_build_swaption_success() {
let result = base_builder().build();
assert!(result.is_ok(), "expected swaption build to succeed");
let swaption = result.unwrap();
assert_eq!(swaption.identifier(), "swaption_test");
assert_eq!(swaption.currency(), Currency::USD);
assert_eq!(swaption.market_index(), MarketIndex::SOFR);
assert_eq!(swaption.strike(), 0.03);
}
#[test]
fn test_build_swaption_missing_swap_tenor_fails() {
let result = MakeSwaption::<DualFwd>::default()
.with_identifier("swaption_missing_tenor".to_string())
.with_expiry(Date::new(2024, 6, 1))
.with_strike(0.03)
.with_notional(1_000_000.0)
.with_rate_definition(sample_rate_definition())
.with_market_index(MarketIndex::SOFR)
.with_currency(Currency::USD)
.build();
assert!(result.is_err(), "expected missing swap tenor to fail");
}
}