use crate::cashflows::{IborCoupon, IborLeg};
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,
settlement_calendar: Option<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>,
fixed_rule: DateGeneration,
float_rule: DateGeneration,
fixed_first_date: Date,
fixed_next_to_last_date: Date,
float_first_date: Date,
float_next_to_last_date: Date,
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,
settlement_calendar: None,
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,
fixed_rule: DateGeneration::Backward,
float_rule: DateGeneration::Backward,
fixed_first_date: Date::null(),
fixed_next_to_last_date: Date::null(),
float_first_date: Date::null(),
float_next_to_last_date: Date::null(),
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_settlement_calendar(mut self, calendar: Calendar) -> MakeVanillaSwap {
self.settlement_calendar = Some(calendar);
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_rule(mut self, rule: DateGeneration) -> MakeVanillaSwap {
self.fixed_rule = rule;
self.float_rule = rule;
self
}
pub fn with_fixed_leg_rule(mut self, rule: DateGeneration) -> MakeVanillaSwap {
self.fixed_rule = rule;
self
}
pub fn with_floating_leg_rule(mut self, rule: DateGeneration) -> MakeVanillaSwap {
self.float_rule = rule;
self
}
pub fn with_fixed_leg_first_date(mut self, d: Date) -> MakeVanillaSwap {
self.fixed_first_date = d;
self
}
pub fn with_fixed_leg_next_to_last_date(mut self, d: Date) -> MakeVanillaSwap {
self.fixed_next_to_last_date = d;
self
}
pub fn with_floating_leg_first_date(mut self, d: Date) -> MakeVanillaSwap {
self.float_first_date = d;
self
}
pub fn with_floating_leg_next_to_last_date(mut self, d: Date) -> MakeVanillaSwap {
self.float_next_to_last_date = d;
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();
validate_schedule_inputs(
start_date,
end_date,
fixed_tenor,
self.fixed_rule,
self.fixed_end_of_month,
self.fixed_first_date,
self.fixed_next_to_last_date,
)?;
validate_schedule_inputs(
start_date,
end_date,
float_tenor,
self.float_rule,
self.float_end_of_month,
self.float_first_date,
self.float_next_to_last_date,
)?;
let fixed_schedule = Schedule::new(
start_date,
end_date,
fixed_tenor,
self.fixed_calendar.clone(),
self.fixed_convention,
self.fixed_termination_date_convention,
self.fixed_rule,
self.fixed_end_of_month,
self.fixed_first_date,
self.fixed_next_to_last_date,
);
let float_schedule = Schedule::new(
start_date,
end_date,
float_tenor,
self.float_calendar.clone(),
self.float_convention,
self.float_termination_date_convention,
self.float_rule,
self.float_end_of_month,
self.float_first_date,
self.float_next_to_last_date,
);
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,
)
}
pub fn floating_leg(&self) -> QlResult<Vec<Shared<IborCoupon>>> {
let start_date = self.start_date()?;
let end_date = self.end_date(start_date);
validate_schedule_inputs(
start_date,
end_date,
self.ibor_index.tenor(),
self.float_rule,
self.float_end_of_month,
self.float_first_date,
self.float_next_to_last_date,
)?;
let float_schedule = Schedule::new(
start_date,
end_date,
self.ibor_index.tenor(),
self.float_calendar.clone(),
self.float_convention,
self.float_termination_date_convention,
self.float_rule,
self.float_end_of_month,
self.float_first_date,
self.float_next_to_last_date,
);
let resolved_convention = float_schedule.business_day_convention();
IborLeg::new(float_schedule, Shared::clone(&self.ibor_index))
.with_notionals(vec![self.nominal])
.with_payment_day_counter(self.ibor_index.day_counter().clone())
.with_payment_adjustment(resolved_convention)
.with_spreads(vec![0.0])
.coupons()
}
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 settlement_calendar = self
.settlement_calendar
.as_ref()
.unwrap_or(&self.float_calendar);
settlement_calendar.advance(
ref_date,
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());
}
}
fn validate_schedule_inputs(
start: Date,
end: Date,
tenor: Period,
rule: DateGeneration,
end_of_month: bool,
first: Date,
next_to_last: Date,
) -> QlResult<()> {
crate::require!(
start != Date::null() && end != Date::null() && start < end,
"invalid schedule date range"
);
crate::require!(tenor.length() >= 0, "non positive schedule tenor");
let effective_rule = if tenor.length() == 0 {
DateGeneration::Zero
} else {
rule
};
let has_first = first != Date::null() && first != start;
let has_next_to_last = next_to_last != Date::null() && next_to_last != end;
crate::require!(
!has_first || !has_next_to_last || first <= next_to_last,
"first stub date later than next-to-last stub date"
);
for (date, initial) in [(first, true), (next_to_last, false)] {
if date == Date::null() || (initial && date == start) || (!initial && date == end) {
continue;
}
match effective_rule {
DateGeneration::Forward | DateGeneration::Backward => {
let valid = if initial {
date > start && date <= end
} else {
date >= start && date < end
};
crate::require!(valid, "stub date outside schedule range");
}
DateGeneration::ThirdWednesday => {
crate::require!(
crate::time::imm::is_imm_date(date, false),
"stub date is not an IMM date"
);
}
_ => crate::fail!("stub date incompatible with {effective_rule} date generation rule"),
}
}
crate::require!(
!end_of_month
|| !allows_end_of_month(tenor)
|| matches!(
effective_rule,
DateGeneration::Forward | DateGeneration::Backward | DateGeneration::Zero
),
"endOfMonth convention incompatible with {effective_rule} date generation rule"
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cashflows::Coupon;
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);
}
fn start_with_settlement_days(
today: Date,
settlement_calendar: Option<Calendar>,
) -> (Date, Shared<IborIndex>) {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(today);
let index = euribor6m(&settings);
let mut maker = MakeVanillaSwap::new(
Period::new(5, TimeUnit::Years),
Shared::clone(&index),
Some(0.03),
Period::new(0, TimeUnit::Days),
settings,
)
.with_settlement_days(2);
if let Some(calendar) = settlement_calendar {
maker = maker.with_settlement_calendar(calendar);
}
let swap = maker.build().unwrap();
let start = swap.fixed_vs_floating().fixed_leg()[0]
.as_coupon()
.unwrap()
.accrual_start_date();
(start, index)
}
#[test]
fn explicit_settlement_days_count_from_a_saturday() {
let today = Date::new(20, Month::June, 2026);
let (start, index) = start_with_settlement_days(today, None);
assert_eq!(start, Date::new(23, Month::June, 2026));
assert_eq!(
start,
index.fixing_calendar().advance(
today,
2,
TimeUnit::Days,
BusinessDayConvention::Following,
false
)
);
}
#[test]
fn settlement_calendar_counts_the_settlement_days() {
use crate::time::calendars::unitedstates::Market;
use crate::time::calendars::{JointCalendar, JointCalendarRule, Target, UnitedStates};
let today = Date::new(2, Month::July, 2026);
let calendar = JointCalendar::of_two(
Target::new(),
UnitedStates::new(Market::Settlement),
JointCalendarRule::JoinHolidays,
);
let (start, index) = start_with_settlement_days(today, Some(calendar));
assert_eq!(start, Date::new(7, Month::July, 2026));
assert_ne!(
start,
index.fixing_calendar().advance(
today,
2,
TimeUnit::Days,
BusinessDayConvention::Following,
false
)
);
}
#[test]
fn settlement_days_fall_back_to_the_floating_leg_calendar() {
use crate::time::calendars::unitedstates::Market;
use crate::time::calendars::{JointCalendar, JointCalendarRule, Target, UnitedStates};
let today = Date::new(2, Month::July, 2026);
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(today);
let index = euribor6m(&settings);
let swap = MakeVanillaSwap::new(
Period::new(5, TimeUnit::Years),
Shared::clone(&index),
Some(0.03),
Period::new(0, TimeUnit::Days),
settings,
)
.with_settlement_days(2)
.with_floating_leg_calendar(JointCalendar::of_two(
Target::new(),
UnitedStates::new(Market::Settlement),
JointCalendarRule::JoinHolidays,
))
.build()
.unwrap();
let start = swap.fixed_vs_floating().fixed_leg()[0]
.as_coupon()
.unwrap()
.accrual_start_date();
assert_eq!(start, Date::new(7, Month::July, 2026));
assert_eq!(
index.fixing_calendar().advance(
today,
2,
TimeUnit::Days,
BusinessDayConvention::Following,
false
),
Date::new(6, Month::July, 2026)
);
}
#[test]
fn index_default_spot_still_adjusts_a_saturday() {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(Date::new(20, Month::June, 2026));
let index = euribor6m(&settings);
let swap = MakeVanillaSwap::new(
Period::new(5, TimeUnit::Years),
index,
Some(0.03),
Period::new(0, TimeUnit::Days),
settings,
)
.build()
.unwrap();
let start = swap.fixed_vs_floating().fixed_leg()[0]
.as_coupon()
.unwrap()
.accrual_start_date();
assert_eq!(start, Date::new(24, Month::June, 2026));
}
#[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());
}
#[test]
fn with_rule_third_wednesday_inclusive_snaps_the_floating_end() {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(Date::new(14, Month::September, 2015));
let index = euribor6m(&settings);
let swap = MakeVanillaSwap::new(
Period::new(1, TimeUnit::Years),
index,
Some(0.03),
Period::new(0, TimeUnit::Days),
Shared::clone(&settings),
)
.with_effective_date(Date::new(16, Month::September, 2015))
.with_rule(DateGeneration::ThirdWednesdayInclusive)
.build()
.unwrap();
let floating = swap.fixed_vs_floating().floating_schedule();
assert_eq!(floating.start_date(), Date::new(16, Month::September, 2015));
assert_eq!(
floating.end_date(),
Date::new(21, Month::September, 2016),
"Inclusive must snap the 16-Sep-2016 maturity to the third Wednesday"
);
}
#[test]
fn stub_first_and_next_to_last_dates_thread_into_schedules() {
use crate::time::calendars::NullCalendar;
let settings = settings_today();
let index = euribor6m(&settings);
let cal = NullCalendar::new();
let effective = Date::new(3, Month::September, 2017);
let termination = Date::new(30, Month::September, 2024);
let float_first = Date::new(31, Month::March, 2018);
let float_ntl = Date::new(15, Month::March, 2024);
let fixed_first = Date::new(30, Month::September, 2018);
let fixed_ntl = Date::new(15, Month::March, 2024);
let base = || {
MakeVanillaSwap::new(
Period::new(0, TimeUnit::Days),
Shared::clone(&index),
Some(0.03),
Period::new(0, TimeUnit::Days),
Shared::clone(&settings),
)
.with_effective_date(effective)
.with_termination_date(termination)
.with_fixed_leg_tenor(Period::new(1, TimeUnit::Years))
.with_fixed_leg_day_count(Thirty360::with_convention(Convention::BondBasis))
.with_fixed_leg_calendar(cal.clone())
.with_fixed_leg_convention(BusinessDayConvention::Unadjusted)
.with_fixed_leg_termination_date_convention(BusinessDayConvention::Unadjusted)
.with_fixed_leg_end_of_month(true)
.with_floating_leg_calendar(cal.clone())
.with_floating_leg_convention(BusinessDayConvention::Unadjusted)
.with_floating_leg_termination_date_convention(BusinessDayConvention::Unadjusted)
.with_floating_leg_end_of_month(true)
};
let swap_first = base()
.with_fixed_leg_first_date(fixed_first)
.with_floating_leg_first_date(float_first)
.with_floating_leg_rule(DateGeneration::Backward)
.build()
.unwrap();
let floating = swap_first.fixed_vs_floating().floating_schedule();
assert_eq!(floating.date(0), effective);
assert_eq!(
floating.date(1),
float_first,
"floating first stub must land on the given first date"
);
assert!(
!floating.is_regular_at(1),
"effective + 6M != first date ⇒ irregular first float period"
);
let fixed = swap_first.fixed_vs_floating().fixed_schedule();
assert_eq!(fixed.date(0), effective);
assert_eq!(
fixed.date(1),
fixed_first,
"fixed first stub must land on the given first date"
);
let swap_float_ntl = base()
.with_floating_leg_next_to_last_date(float_ntl)
.with_floating_leg_rule(DateGeneration::Forward)
.build()
.unwrap();
let floating = swap_float_ntl.fixed_vs_floating().floating_schedule();
let n = floating.len();
assert_eq!(
floating.date(n - 2),
float_ntl,
"floating next-to-last stub must land on the given next-to-last date"
);
assert!(
!floating.is_regular_at(n - 2),
"period ending at off-grid next-to-last must be irregular"
);
let hand_float = Schedule::new(
effective,
termination,
index.tenor(),
cal.clone(),
BusinessDayConvention::Unadjusted,
BusinessDayConvention::Unadjusted,
DateGeneration::Forward,
true,
Date::null(),
float_ntl,
);
let made_dates: Vec<Date> = (0..floating.len()).map(|i| floating.date(i)).collect();
let hand_dates: Vec<Date> = (0..hand_float.len()).map(|i| hand_float.date(i)).collect();
assert_eq!(
made_dates, hand_dates,
"maker floating schedule must match a hand-built Schedule with the same stubs"
);
let swap_fixed_ntl = base()
.with_fixed_leg_next_to_last_date(fixed_ntl)
.with_fixed_leg_rule(DateGeneration::Forward)
.build()
.unwrap();
let fixed = swap_fixed_ntl.fixed_vs_floating().fixed_schedule();
let n = fixed.len();
assert_eq!(
fixed.date(n - 2),
fixed_ntl,
"fixed next-to-last stub must land on the given next-to-last date"
);
assert!(
!fixed.is_regular_at(n - 2),
"period ending at off-grid fixed next-to-last must be irregular"
);
let hand_fixed = Schedule::new(
effective,
termination,
Period::new(1, TimeUnit::Years),
cal.clone(),
BusinessDayConvention::Unadjusted,
BusinessDayConvention::Unadjusted,
DateGeneration::Forward,
true,
Date::null(),
fixed_ntl,
);
let made_fixed: Vec<Date> = (0..fixed.len()).map(|i| fixed.date(i)).collect();
let hand_fixed_dates: Vec<Date> =
(0..hand_fixed.len()).map(|i| hand_fixed.date(i)).collect();
assert_eq!(
made_fixed, hand_fixed_dates,
"maker fixed schedule must match a hand-built Schedule with the same stubs"
);
let maker = base()
.with_floating_leg_first_date(float_first)
.with_floating_leg_rule(DateGeneration::Backward);
let built = maker.build().unwrap();
let built_starts: Vec<Date> = built
.fixed_vs_floating()
.floating_schedule()
.dates()
.windows(2)
.map(|w| w[0])
.collect();
let maker = base()
.with_floating_leg_first_date(float_first)
.with_floating_leg_rule(DateGeneration::Backward);
let leg = maker.floating_leg().unwrap();
let leg_starts: Vec<Date> = leg.iter().map(|c| c.accrual_start_date()).collect();
assert_eq!(
leg_starts, built_starts,
"floating_leg coupon starts must match build() floating schedule periods"
);
assert_eq!(leg_starts[0], effective);
assert_eq!(leg_starts[1], float_first);
}
#[test]
fn invalid_stub_controls_return_errors_instead_of_schedule_panics() {
let settings = settings_today();
let index = euribor6m(&settings);
let start = Date::new(9, Month::July, 2026);
let make = || {
MakeVanillaSwap::new(
Period::new(5, TimeUnit::Years),
Shared::clone(&index),
Some(0.03),
Period::new(0, TimeUnit::Days),
Shared::clone(&settings),
)
.with_effective_date(start)
};
assert!(make().with_fixed_leg_first_date(start - 1).build().is_err());
assert!(
make()
.with_floating_leg_next_to_last_date(start - 1)
.floating_leg()
.is_err()
);
assert!(
make()
.with_rule(DateGeneration::Twentieth)
.with_fixed_leg_first_date(start + 1)
.build()
.is_err()
);
assert!(
make()
.with_floating_leg_rule(DateGeneration::ThirdWednesday)
.with_floating_leg_end_of_month(true)
.floating_leg()
.is_err()
);
assert!(
make()
.with_fixed_leg_rule(DateGeneration::ThirdWednesday)
.with_fixed_leg_first_date(start + 1)
.build()
.is_err()
);
}
#[test]
fn combined_distinct_leg_stubs_match_quantlib_forward_and_backward() {
use crate::time::calendars::NullCalendar;
let settings = settings_today();
let index = euribor6m(&settings);
let d = |month, year| Date::new(15, month, year);
let cases = [
(
DateGeneration::Forward,
vec![
d(Month::January, 2026),
d(Month::July, 2026),
d(Month::July, 2027),
d(Month::July, 2028),
d(Month::October, 2028),
d(Month::January, 2029),
],
vec![
d(Month::January, 2026),
d(Month::April, 2026),
d(Month::October, 2026),
d(Month::April, 2027),
d(Month::October, 2027),
d(Month::April, 2028),
d(Month::October, 2028),
d(Month::November, 2028),
d(Month::January, 2029),
],
),
(
DateGeneration::Backward,
vec![
d(Month::January, 2026),
d(Month::July, 2026),
d(Month::October, 2026),
d(Month::October, 2027),
d(Month::October, 2028),
d(Month::January, 2029),
],
vec![
d(Month::January, 2026),
d(Month::April, 2026),
d(Month::May, 2026),
d(Month::November, 2026),
d(Month::May, 2027),
d(Month::November, 2027),
d(Month::May, 2028),
d(Month::November, 2028),
d(Month::January, 2029),
],
),
];
for (rule, fixed_dates, floating_dates) in cases {
let swap = MakeVanillaSwap::new(
Period::new(3, TimeUnit::Years),
Shared::clone(&index),
Some(0.03),
Period::new(0, TimeUnit::Days),
Shared::clone(&settings),
)
.with_effective_date(d(Month::January, 2026))
.with_rule(rule)
.with_fixed_leg_calendar(NullCalendar::new())
.with_floating_leg_calendar(NullCalendar::new())
.with_fixed_leg_convention(BusinessDayConvention::Unadjusted)
.with_floating_leg_convention(BusinessDayConvention::Unadjusted)
.with_fixed_leg_termination_date_convention(BusinessDayConvention::Unadjusted)
.with_floating_leg_termination_date_convention(BusinessDayConvention::Unadjusted)
.with_fixed_leg_first_date(d(Month::July, 2026))
.with_fixed_leg_next_to_last_date(d(Month::October, 2028))
.with_floating_leg_first_date(d(Month::April, 2026))
.with_floating_leg_next_to_last_date(d(Month::November, 2028))
.build()
.unwrap();
assert_eq!(
swap.fixed_vs_floating().fixed_schedule().dates(),
fixed_dates.as_slice()
);
assert_eq!(
swap.fixed_vs_floating().floating_schedule().dates(),
floating_dates.as_slice()
);
}
}
#[test]
fn schedule_validation_normalizes_eom_and_rejects_reversed_active_stubs() {
let start = Date::new(15, Month::January, 2026);
let end = Date::new(15, Month::January, 2029);
assert!(
validate_schedule_inputs(
start,
end,
Period::new(1, TimeUnit::Weeks),
DateGeneration::ThirdWednesday,
true,
Date::null(),
Date::null()
)
.is_ok()
);
assert!(
validate_schedule_inputs(
start,
end,
Period::new(1, TimeUnit::Years),
DateGeneration::Forward,
false,
end - 1,
start + 1
)
.is_err()
);
}
}