use crate::cashflow::{CashFlow, Leg};
use crate::cashflows::{Coupon, IborCoupon, IborLeg};
use crate::errors::QlResult;
use crate::indexes::IborIndex;
use crate::instrument::{Instrument, InstrumentBase};
use crate::instruments::fixedvsfloatingswap::{
FixedVsFloatingSwap, FixedVsFloatingSwapArguments, FloatingArgumentsFn,
};
use crate::instruments::swap::SwapType;
use crate::pricingengine::{Arguments, Results};
use crate::settings::Settings;
use crate::shared::Shared;
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::date::Date;
use crate::time::daycounter::DayCounter;
use crate::time::schedule::Schedule;
use crate::types::{Rate, Real, Spread};
pub struct VanillaSwap {
base: FixedVsFloatingSwap,
}
impl VanillaSwap {
#[allow(clippy::too_many_arguments)]
pub fn new(
swap_type: SwapType,
nominal: Real,
fixed_schedule: Schedule,
fixed_rate: Rate,
fixed_day_count: DayCounter,
float_schedule: Schedule,
ibor_index: Shared<IborIndex>,
spread: Spread,
floating_day_count: DayCounter,
payment_convention: Option<BusinessDayConvention>,
settings: Shared<Settings<Date>>,
) -> QlResult<VanillaSwap> {
let resolved_convention =
payment_convention.unwrap_or_else(|| float_schedule.business_day_convention());
let coupons = IborLeg::new(float_schedule.clone(), Shared::clone(&ibor_index))
.with_notionals(vec![nominal])
.with_payment_day_counter(floating_day_count.clone())
.with_payment_adjustment(resolved_convention)
.with_spreads(vec![spread])
.coupons()?;
let floating_leg: Leg = coupons
.iter()
.map(|coupon| Shared::clone(coupon) as Shared<dyn CashFlow>)
.collect();
let floating_arguments: FloatingArgumentsFn =
Box::new(move |swap, args| fill_floating_arguments(&coupons, swap, args));
let base = FixedVsFloatingSwap::new(
swap_type,
vec![nominal],
fixed_schedule,
fixed_rate,
Some(fixed_day_count),
vec![nominal],
float_schedule,
ibor_index,
spread,
floating_day_count,
payment_convention,
0,
None,
floating_leg,
floating_arguments,
settings,
)?;
Ok(VanillaSwap { base })
}
pub fn fixed_vs_floating(&self) -> &FixedVsFloatingSwap {
&self.base
}
pub fn fixed_vs_floating_mut(&mut self) -> &mut FixedVsFloatingSwap {
&mut self.base
}
pub fn into_fixed_vs_floating(self) -> FixedVsFloatingSwap {
self.base
}
}
fn fill_floating_arguments(
coupons: &[Shared<IborCoupon>],
swap: &FixedVsFloatingSwap,
args: &mut FixedVsFloatingSwapArguments,
) -> QlResult<()> {
let n = coupons.len();
args.floating_reset_dates = Vec::with_capacity(n);
args.floating_pay_dates = Vec::with_capacity(n);
args.floating_nominals = Vec::with_capacity(n);
args.floating_fixing_dates = Vec::with_capacity(n);
args.floating_accrual_times = Vec::with_capacity(n);
args.floating_spreads = Vec::with_capacity(n);
args.floating_coupons = Vec::with_capacity(n);
for coupon in coupons {
args.floating_reset_dates.push(coupon.accrual_start_date());
args.floating_pay_dates
.push(coupon.coupon_base().payment_date());
args.floating_nominals.push(coupon.nominal());
args.floating_fixing_dates.push(coupon.fixing_date());
args.floating_accrual_times.push(coupon.accrual_period());
args.floating_spreads.push(swap.spread());
args.floating_coupons.push(Coupon::amount(coupon.as_ref())?);
}
Ok(())
}
impl Instrument for VanillaSwap {
fn base(&self) -> &InstrumentBase {
self.base.base()
}
fn base_mut(&mut self) -> &mut InstrumentBase {
self.base.base_mut()
}
fn is_expired(&self) -> QlResult<bool> {
self.base.is_expired()
}
fn setup_expired(&mut self) {
self.base.setup_expired();
}
fn setup_arguments(&self, arguments: &mut dyn Arguments) -> QlResult<()> {
self.base.setup_arguments(arguments)
}
fn fetch_results(&mut self, results: &dyn Results) -> QlResult<()> {
self.base.fetch_results(results)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::handle::Handle;
use crate::indexes::ibor::Euribor;
use crate::instruments::swap::SwapArguments;
use crate::interestrate::Compounding;
use crate::shared::shared;
use crate::termstructures::yields::FlatForward;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::calendars::target::Target;
use crate::time::date::Month;
use crate::time::daycounters::actual360::Actual360;
use crate::time::frequency::Frequency;
use crate::time::schedule::MakeSchedule;
const NOMINAL: Real = 1_000_000.0;
const FIXED_RATE: Rate = 0.05;
const SPREAD: Spread = 0.001;
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 euribor(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::three_months(curve, Shared::clone(settings)))
}
fn schedule() -> Schedule {
MakeSchedule::new()
.from(Date::new(7, Month::July, 2027))
.to(Date::new(7, Month::July, 2029))
.with_frequency(Frequency::Annual)
.with_calendar(Target::new())
.with_convention(BusinessDayConvention::Following)
.build()
}
fn make_swap(swap_type: SwapType) -> VanillaSwap {
let settings = settings_today();
let index = euribor(&settings);
VanillaSwap::new(
swap_type,
NOMINAL,
schedule(),
FIXED_RATE,
Actual360::new(),
schedule(),
index,
SPREAD,
Actual360::new(),
None,
settings,
)
.unwrap()
}
#[test]
fn it_builds_both_legs_and_exposes_the_base() {
let swap = make_swap(SwapType::Payer);
let base = swap.fixed_vs_floating();
assert_eq!(base.fixed_leg().len(), 2, "two annual fixed coupons");
assert_eq!(base.floating_leg().len(), 2, "two annual ibor coupons");
assert_eq!(base.swap_type(), SwapType::Payer);
assert_eq!(base.fixed_rate(), FIXED_RATE);
assert_eq!(base.spread(), SPREAD);
assert_eq!(base.nominal().unwrap(), NOMINAL);
}
#[test]
fn payer_type_maps_the_leg_multipliers_through_delegation() {
let mut payer = SwapArguments::default();
make_swap(SwapType::Payer)
.setup_arguments(&mut payer)
.unwrap();
assert_eq!(payer.payer, vec![-1.0, 1.0], "payer: -fixed, +floating");
let mut receiver = SwapArguments::default();
make_swap(SwapType::Receiver)
.setup_arguments(&mut receiver)
.unwrap();
assert_eq!(
receiver.payer,
vec![1.0, -1.0],
"receiver: +fixed, -floating"
);
}
#[test]
fn the_floating_hook_fills_the_argument_vectors() {
let swap = make_swap(SwapType::Payer);
let mut args = FixedVsFloatingSwapArguments::default();
swap.setup_arguments(&mut args).unwrap();
let leg = swap.fixed_vs_floating().floating_leg();
let n = leg.len();
assert_eq!(n, 2);
let coupons: Vec<&dyn Coupon> = leg.iter().map(|f| f.as_coupon().unwrap()).collect();
let expected_resets: Vec<Date> = coupons.iter().map(|c| c.accrual_start_date()).collect();
assert_eq!(args.floating_reset_dates, expected_resets);
let expected_pays: Vec<Date> = leg.iter().map(|f| f.date()).collect();
assert_eq!(args.floating_pay_dates, expected_pays);
assert_eq!(args.floating_nominals, vec![NOMINAL; n]);
let expected_accruals: Vec<_> = coupons.iter().map(|c| c.accrual_period()).collect();
assert_eq!(args.floating_accrual_times, expected_accruals);
assert_eq!(
args.floating_spreads,
vec![SPREAD; n],
"one spread per coupon"
);
let expected_amounts: Vec<Real> = leg.iter().map(|f| f.amount().unwrap()).collect();
assert_eq!(args.floating_coupons, expected_amounts);
assert!(
args.floating_coupons.iter().all(|&a| a > 0.0),
"forecast coupon amounts are positive"
);
assert_eq!(args.floating_fixing_dates.len(), n);
for (fixing, reset) in args.floating_fixing_dates.iter().zip(&expected_resets) {
assert!(fixing <= reset, "fixing precedes accrual start");
}
}
#[test]
fn into_fixed_vs_floating_yields_the_intact_base() {
let base = make_swap(SwapType::Receiver).into_fixed_vs_floating();
assert_eq!(base.swap_type(), SwapType::Receiver);
assert_eq!(base.fixed_rate(), FIXED_RATE);
assert_eq!(base.spread(), SPREAD);
assert_eq!(base.fixed_leg().len(), 2);
assert_eq!(base.floating_leg().len(), 2);
}
#[test]
fn fair_values_are_unavailable_before_pricing() {
let mut swap = make_swap(SwapType::Payer);
assert!(swap.fixed_vs_floating_mut().fair_rate().is_err());
assert!(swap.fixed_vs_floating_mut().fair_spread().is_err());
}
}