use crate::currency::Currency;
use crate::errors::QlResult;
use crate::handle::Handle;
use crate::indexes::IborIndex;
use crate::indexes::index::Index;
use crate::indexes::interestrateindex::InterestRateIndex;
use crate::instrument::Instrument;
use crate::instruments::swap::SwapType;
use crate::pricingengine::PricingEngine;
use crate::pricingengines::DiscountingSwapEngine;
use crate::settings::Settings;
use crate::shared::{Shared, SharedMut, shared_mut};
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendar::Calendar;
use crate::time::date::Date;
use crate::time::dategenerationrule::DateGeneration;
use crate::time::daycounter::DayCounter;
use crate::time::daycounters::actual360::Actual360;
use crate::time::daycounters::thirty360::{Convention, Thirty360};
use crate::time::period::Period;
use crate::time::schedule::{Schedule, allows_end_of_month};
use crate::time::timeunit::TimeUnit;
use crate::types::{Integer, Natural, Rate, Real};
use super::VanillaSwap;
pub struct MakeVanillaSwap {
swap_tenor: Period,
ibor_index: Shared<IborIndex>,
fixed_rate: Option<Rate>,
forward_start: Period,
settings: Shared<Settings<Date>>,
settlement_days: Option<Natural>,
effective_date: Option<Date>,
termination_date: Option<Date>,
nominal: Real,
fixed_calendar: Calendar,
float_calendar: Calendar,
fixed_tenor: Option<Period>,
fixed_convention: BusinessDayConvention,
fixed_termination_date_convention: BusinessDayConvention,
float_convention: BusinessDayConvention,
float_termination_date_convention: BusinessDayConvention,
fixed_end_of_month: bool,
float_end_of_month: bool,
fixed_day_count: Option<DayCounter>,
use_indexed_coupons: Option<bool>,
discounting_curve: Option<Handle<dyn YieldTermStructure>>,
}
impl MakeVanillaSwap {
pub fn new(
swap_tenor: Period,
ibor_index: Shared<IborIndex>,
fixed_rate: Option<Rate>,
forward_start: Period,
settings: Shared<Settings<Date>>,
) -> MakeVanillaSwap {
let fixing_calendar = ibor_index.fixing_calendar();
let index_convention = ibor_index.business_day_convention();
MakeVanillaSwap {
swap_tenor,
ibor_index,
fixed_rate,
forward_start,
settings,
settlement_days: None,
effective_date: None,
termination_date: None,
nominal: 1.0,
fixed_calendar: fixing_calendar.clone(),
float_calendar: fixing_calendar,
fixed_tenor: None,
fixed_convention: BusinessDayConvention::ModifiedFollowing,
fixed_termination_date_convention: BusinessDayConvention::ModifiedFollowing,
float_convention: index_convention,
float_termination_date_convention: index_convention,
fixed_end_of_month: false,
float_end_of_month: false,
fixed_day_count: None,
use_indexed_coupons: None,
discounting_curve: None,
}
}
pub fn with_effective_date(mut self, effective_date: Date) -> MakeVanillaSwap {
self.effective_date = Some(effective_date);
self
}
pub fn with_termination_date(mut self, termination_date: Date) -> MakeVanillaSwap {
self.termination_date = Some(termination_date);
self.swap_tenor = Period::new(0, TimeUnit::Days);
self
}
pub fn with_settlement_days(mut self, settlement_days: Natural) -> MakeVanillaSwap {
self.settlement_days = Some(settlement_days);
self
}
pub fn with_nominal(mut self, nominal: Real) -> MakeVanillaSwap {
self.nominal = nominal;
self
}
pub fn with_fixed_leg_tenor(mut self, tenor: Period) -> MakeVanillaSwap {
self.fixed_tenor = Some(tenor);
self
}
pub fn with_fixed_leg_day_count(mut self, day_count: DayCounter) -> MakeVanillaSwap {
self.fixed_day_count = Some(day_count);
self
}
pub fn with_fixed_leg_calendar(mut self, calendar: Calendar) -> MakeVanillaSwap {
self.fixed_calendar = calendar;
self
}
pub fn with_fixed_leg_convention(mut self, bdc: BusinessDayConvention) -> MakeVanillaSwap {
self.fixed_convention = bdc;
self
}
pub fn with_fixed_leg_termination_date_convention(
mut self,
bdc: BusinessDayConvention,
) -> MakeVanillaSwap {
self.fixed_termination_date_convention = bdc;
self
}
pub fn with_fixed_leg_end_of_month(mut self, flag: bool) -> MakeVanillaSwap {
self.fixed_end_of_month = flag;
self
}
pub fn with_floating_leg_calendar(mut self, calendar: Calendar) -> MakeVanillaSwap {
self.float_calendar = calendar;
self
}
pub fn with_floating_leg_convention(mut self, bdc: BusinessDayConvention) -> MakeVanillaSwap {
self.float_convention = bdc;
self
}
pub fn with_floating_leg_termination_date_convention(
mut self,
bdc: BusinessDayConvention,
) -> MakeVanillaSwap {
self.float_termination_date_convention = bdc;
self
}
pub fn with_floating_leg_end_of_month(mut self, flag: bool) -> MakeVanillaSwap {
self.float_end_of_month = flag;
self
}
pub fn with_discounting_term_structure(
mut self,
discounting_term_structure: Handle<dyn YieldTermStructure>,
) -> MakeVanillaSwap {
self.discounting_curve = Some(discounting_term_structure);
self
}
pub fn with_indexed_coupons(mut self, use_indexed_coupons: Option<bool>) -> MakeVanillaSwap {
self.use_indexed_coupons = use_indexed_coupons;
self
}
pub fn build(self) -> QlResult<VanillaSwap> {
if self.effective_date.is_some() && self.settlement_days.is_some() {
crate::fail!(
"cannot set both an explicit effective date and settlement days; use one or the other"
);
}
if let Some(requested_indexed) = self.use_indexed_coupons {
let effective_indexed = !self.settings.using_at_par_coupons();
if requested_indexed != effective_indexed {
crate::fail!(
"with_indexed_coupons({requested_indexed}) conflicts with Settings::using_at_par_coupons(): \
the per-swap override is deferred with VanillaSwap, so the coupon mode must match Settings"
);
}
}
let start_date = self.start_date()?;
let end_date = self.end_date(start_date);
let currency = self.ibor_index.currency().clone();
let fixed_tenor = match self.fixed_tenor {
Some(tenor) => tenor,
None => default_fixed_tenor(¤cy)?,
};
let fixed_day_count = match &self.fixed_day_count {
Some(day_count) => day_count.clone(),
None => default_fixed_day_count(¤cy)?,
};
let float_tenor = self.ibor_index.tenor();
let float_day_count = self.ibor_index.day_counter().clone();
let fixed_schedule = Schedule::new(
start_date,
end_date,
fixed_tenor,
self.fixed_calendar.clone(),
self.fixed_convention,
self.fixed_termination_date_convention,
DateGeneration::Backward,
self.fixed_end_of_month,
Date::null(),
Date::null(),
);
let float_schedule = Schedule::new(
start_date,
end_date,
float_tenor,
self.float_calendar.clone(),
self.float_convention,
self.float_termination_date_convention,
DateGeneration::Backward,
self.float_end_of_month,
Date::null(),
Date::null(),
);
let used_fixed_rate = match self.fixed_rate {
Some(fixed_rate) => fixed_rate,
None => {
let mut temp = self.assemble(
0.0,
fixed_schedule.clone(),
float_schedule.clone(),
fixed_day_count.clone(),
float_day_count.clone(),
)?;
temp.fixed_vs_floating_mut().fair_rate()?
}
};
self.assemble(
used_fixed_rate,
fixed_schedule,
float_schedule,
fixed_day_count,
float_day_count,
)
}
fn start_date(&self) -> QlResult<Date> {
if let Some(effective_date) = self.effective_date {
return Ok(effective_date);
}
let ref_date = match self.settings.evaluation_date() {
Some(today) => today,
None => crate::fail!(
"no evaluation date set: MakeVanillaSwap needs a reference date to derive the start date"
),
};
let spot_date = match self.settlement_days {
None => {
let adjusted = self
.ibor_index
.fixing_calendar()
.adjust(ref_date, BusinessDayConvention::Following);
self.ibor_index.value_date(adjusted)?
}
Some(settlement_days) => {
let adjusted = self
.float_calendar
.adjust(ref_date, BusinessDayConvention::Following);
self.float_calendar.advance(
adjusted,
settlement_days as Integer,
TimeUnit::Days,
BusinessDayConvention::Following,
false,
)
}
};
let start = spot_date + self.forward_start;
Ok(if self.forward_start.length() < 0 {
self.float_calendar
.adjust(start, BusinessDayConvention::Preceding)
} else if self.forward_start.length() > 0 {
self.float_calendar
.adjust(start, BusinessDayConvention::Following)
} else {
start
})
}
fn end_date(&self, start_date: Date) -> Date {
if let Some(termination_date) = self.termination_date {
return termination_date;
}
let mut end_date = start_date + self.swap_tenor;
if self.float_end_of_month
&& allows_end_of_month(self.swap_tenor)
&& self.float_calendar.is_end_of_month(start_date)
{
end_date = self.float_calendar.end_of_month(end_date);
}
end_date
}
fn assemble(
&self,
fixed_rate: Rate,
fixed_schedule: Schedule,
float_schedule: Schedule,
fixed_day_count: DayCounter,
float_day_count: DayCounter,
) -> QlResult<VanillaSwap> {
let mut swap = VanillaSwap::new(
SwapType::Payer,
self.nominal,
fixed_schedule,
fixed_rate,
fixed_day_count,
float_schedule,
Shared::clone(&self.ibor_index),
0.0,
float_day_count,
None,
Shared::clone(&self.settings),
)?;
let discount_curve = match &self.discounting_curve {
Some(curve) => curve.clone(),
None => self.ibor_index.forwarding_term_structure().clone(),
};
let engine = shared_mut(DiscountingSwapEngine::new(
discount_curve,
Some(false),
None,
None,
Shared::clone(&self.settings),
));
swap.base_mut()
.set_pricing_engine(engine as SharedMut<dyn PricingEngine>);
Ok(swap)
}
}
fn default_fixed_tenor(currency: &Currency) -> QlResult<Period> {
if *currency == Currency::eur() || *currency == Currency::usd() {
Ok(Period::new(1, TimeUnit::Years))
} else {
crate::fail!("unknown fixed leg default tenor for {}", currency.code());
}
}
fn default_fixed_day_count(currency: &Currency) -> QlResult<DayCounter> {
if *currency == Currency::usd() {
Ok(Actual360::new())
} else if *currency == Currency::eur() {
Ok(Thirty360::with_convention(Convention::BondBasis))
} else {
crate::fail!("unknown fixed leg day counter for {}", currency.code());
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::indexes::ibor::Euribor;
use crate::instrument::Instrument;
use crate::interestrate::Compounding;
use crate::shared::shared;
use crate::termstructures::yields::FlatForward;
use crate::time::date::Month;
use crate::time::daycounters::actual360::Actual360;
use crate::time::frequency::Frequency;
fn today() -> Date {
Date::new(7, Month::July, 2026)
}
fn settings_today() -> Shared<Settings<Date>> {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(today());
settings
}
fn euribor6m(settings: &Shared<Settings<Date>>) -> Shared<IborIndex> {
let curve = Handle::new(shared(FlatForward::with_rate(
today(),
0.02,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>);
shared(Euribor::six_months(curve, Shared::clone(settings)))
}
fn null() -> Date {
Date::null()
}
#[test]
fn maker_matches_hand_built_swap() {
let settings = settings_today();
let index = euribor6m(&settings);
let effective = Date::new(9, Month::July, 2026);
let tenor = Period::new(5, TimeUnit::Years);
let fixed_rate = 0.03;
let mut made = MakeVanillaSwap::new(
tenor,
Shared::clone(&index),
Some(fixed_rate),
Period::new(0, TimeUnit::Days),
Shared::clone(&settings),
)
.with_effective_date(effective)
.build()
.unwrap();
let calendar = index.fixing_calendar();
let end = effective + tenor;
let fixed_schedule = Schedule::new(
effective,
end,
Period::new(1, TimeUnit::Years),
calendar.clone(),
BusinessDayConvention::ModifiedFollowing,
BusinessDayConvention::ModifiedFollowing,
DateGeneration::Backward,
false,
null(),
null(),
);
let float_schedule = Schedule::new(
effective,
end,
index.tenor(),
calendar,
index.business_day_convention(),
index.business_day_convention(),
DateGeneration::Backward,
false,
null(),
null(),
);
let mut hand = VanillaSwap::new(
SwapType::Payer,
1.0,
fixed_schedule,
fixed_rate,
Thirty360::with_convention(Convention::BondBasis),
float_schedule,
Shared::clone(&index),
0.0,
index.day_counter().clone(),
None,
Shared::clone(&settings),
)
.unwrap();
let engine = shared_mut(DiscountingSwapEngine::new(
index.forwarding_term_structure().clone(),
Some(false),
None,
None,
Shared::clone(&settings),
));
hand.base_mut()
.set_pricing_engine(engine as SharedMut<dyn PricingEngine>);
let made_fixed: Vec<Date> = made
.fixed_vs_floating()
.fixed_leg()
.iter()
.map(|f| f.date())
.collect();
let hand_fixed: Vec<Date> = hand
.fixed_vs_floating()
.fixed_leg()
.iter()
.map(|f| f.date())
.collect();
assert_eq!(made_fixed, hand_fixed, "fixed leg payment dates");
let made_float: Vec<Date> = made
.fixed_vs_floating()
.floating_leg()
.iter()
.map(|f| f.date())
.collect();
let hand_float: Vec<Date> = hand
.fixed_vs_floating()
.floating_leg()
.iter()
.map(|f| f.date())
.collect();
assert_eq!(made_float, hand_float, "floating leg payment dates");
assert!(
(made.npv().unwrap() - hand.npv().unwrap()).abs() < 1.0e-14,
"maker NPV {} vs hand-built NPV {}",
made.npv().unwrap(),
hand.npv().unwrap()
);
}
#[test]
fn derived_start_is_the_index_spot_date() {
let settings = settings_today();
let index = euribor6m(&settings);
let swap = MakeVanillaSwap::new(
Period::new(1, TimeUnit::Years),
Shared::clone(&index),
Some(0.03),
Period::new(0, TimeUnit::Days),
Shared::clone(&settings),
)
.build()
.unwrap();
let adjusted = index
.fixing_calendar()
.adjust(today(), BusinessDayConvention::Following);
let expected_spot = index.value_date(adjusted).unwrap();
let first_start = swap.fixed_vs_floating().fixed_leg()[0]
.as_coupon()
.unwrap()
.accrual_start_date();
assert_eq!(first_start, expected_spot);
}
#[test]
fn eur_default_fixed_tenor_is_annual() {
let settings = settings_today();
let index = euribor6m(&settings);
let swap = MakeVanillaSwap::new(
Period::new(2, TimeUnit::Years),
index,
Some(0.03),
Period::new(0, TimeUnit::Days),
Shared::clone(&settings),
)
.with_effective_date(Date::new(9, Month::July, 2026))
.build()
.unwrap();
assert_eq!(swap.fixed_vs_floating().fixed_leg().len(), 2);
assert_eq!(swap.fixed_vs_floating().floating_leg().len(), 4);
}
#[test]
fn end_of_month_rolls_the_maturity() {
let settings = settings_today();
let index = euribor6m(&settings);
let calendar = index.fixing_calendar();
let effective = calendar.end_of_month(Date::new(15, Month::April, 2027));
assert!(calendar.is_end_of_month(effective), "start is month-end");
let swap = MakeVanillaSwap::new(
Period::new(2, TimeUnit::Years),
Shared::clone(&index),
Some(0.03),
Period::new(0, TimeUnit::Days),
Shared::clone(&settings),
)
.with_effective_date(effective)
.with_floating_leg_end_of_month(true)
.build()
.unwrap();
let maturity = swap
.fixed_vs_floating()
.floating_leg()
.last()
.unwrap()
.date();
assert!(
calendar.is_end_of_month(maturity),
"maturity {maturity:?} is a month-end business day"
);
}
#[test]
fn unset_fixed_rate_fills_the_fair_rate() {
let settings = settings_today();
let index = euribor6m(&settings);
let mut swap = MakeVanillaSwap::new(
Period::new(5, TimeUnit::Years),
index,
None,
Period::new(0, TimeUnit::Days),
Shared::clone(&settings),
)
.with_effective_date(Date::new(9, Month::July, 2026))
.build()
.unwrap();
assert!(
swap.npv().unwrap().abs() < 1.0e-8,
"fair-rate swap prices to par, got {}",
swap.npv().unwrap()
);
}
#[test]
fn indexed_coupon_request_is_checked_against_settings() {
let settings = settings_today();
let index = euribor6m(&settings);
let effective = Date::new(9, Month::July, 2026);
let make = |flag: Option<bool>| {
MakeVanillaSwap::new(
Period::new(1, TimeUnit::Years),
Shared::clone(&index),
Some(0.03),
Period::new(0, TimeUnit::Days),
Shared::clone(&settings),
)
.with_effective_date(effective)
.with_indexed_coupons(flag)
.build()
};
assert!(
make(Some(true)).is_err(),
"indexed conflicts with at-par default"
);
assert!(make(Some(false)).is_ok(), "at-par agrees with the default");
assert!(make(None).is_ok(), "unset defers to Settings");
}
#[test]
fn effective_date_and_settlement_days_conflict() {
let settings = settings_today();
let index = euribor6m(&settings);
let result = MakeVanillaSwap::new(
Period::new(1, TimeUnit::Years),
index,
Some(0.03),
Period::new(0, TimeUnit::Days),
Shared::clone(&settings),
)
.with_effective_date(Date::new(9, Month::July, 2026))
.with_settlement_days(2)
.build();
assert!(result.is_err());
}
}