use std::any::Any;
use crate::cashflow::Leg;
use crate::cashflows::FixedRateLeg;
use crate::errors::QlResult;
use crate::indexes::{IborIndex, InterestRateIndex};
use crate::instrument::{Instrument, InstrumentBase, InstrumentResults};
use crate::instruments::swap::{Swap, SwapArguments, SwapResults, SwapType};
use crate::interestrate::Compounding;
use crate::pricingengine::{Arguments, GenericEngine, Results};
use crate::settings::Settings;
use crate::shared::Shared;
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendar::Calendar;
use crate::time::date::Date;
use crate::time::daycounter::DayCounter;
use crate::time::frequency::Frequency;
use crate::time::schedule::Schedule;
use crate::types::{Integer, Rate, Real, Spread, Time};
use crate::{fail, require};
const BASIS_POINT: Real = 1.0e-4;
pub type FloatingArgumentsFn =
Box<dyn Fn(&FixedVsFloatingSwap, &mut FixedVsFloatingSwapArguments) -> QlResult<()>>;
#[derive(Default)]
pub struct FixedVsFloatingSwapArguments {
pub swap: SwapArguments,
pub swap_type: Option<SwapType>,
pub nominal: Option<Real>,
pub fixed_nominals: Vec<Real>,
pub fixed_reset_dates: Vec<Date>,
pub fixed_pay_dates: Vec<Date>,
pub floating_nominals: Vec<Real>,
pub floating_accrual_times: Vec<Time>,
pub floating_reset_dates: Vec<Date>,
pub floating_fixing_dates: Vec<Date>,
pub floating_pay_dates: Vec<Date>,
pub fixed_coupons: Vec<Real>,
pub floating_spreads: Vec<Spread>,
pub floating_coupons: Vec<Real>,
}
impl Arguments for FixedVsFloatingSwapArguments {
fn validate(&self) -> QlResult<()> {
self.swap.validate()?;
require!(
self.fixed_nominals.len() == self.fixed_pay_dates.len(),
"number of fixed nominals different from number of fixed payment dates"
);
require!(
self.fixed_reset_dates.len() == self.fixed_pay_dates.len(),
"number of fixed start dates different from number of fixed payment dates"
);
require!(
self.fixed_pay_dates.len() == self.fixed_coupons.len(),
"number of fixed payment dates different from number of fixed coupon amounts"
);
require!(
self.floating_nominals.len() == self.floating_pay_dates.len(),
"number of floating nominals different from number of floating payment dates"
);
require!(
self.floating_reset_dates.len() == self.floating_pay_dates.len(),
"number of floating start dates different from number of floating payment dates"
);
require!(
self.floating_fixing_dates.len() == self.floating_pay_dates.len(),
"number of floating fixing dates different from number of floating payment dates"
);
require!(
self.floating_accrual_times.len() == self.floating_pay_dates.len(),
"number of floating accrual Times different from number of floating payment dates"
);
require!(
self.floating_spreads.len() == self.floating_pay_dates.len(),
"number of floating spreads different from number of floating payment dates"
);
require!(
self.floating_pay_dates.len() == self.floating_coupons.len(),
"number of floating payment dates different from number of floating coupon amounts"
);
Ok(())
}
}
#[derive(Default)]
pub struct FixedVsFloatingSwapResults {
pub swap: SwapResults,
pub fair_rate: Option<Rate>,
pub fair_spread: Option<Spread>,
}
impl Results for FixedVsFloatingSwapResults {
fn reset(&mut self) {
self.swap.reset();
self.fair_rate = None;
self.fair_spread = None;
}
fn as_instrument_results(&self) -> Option<&InstrumentResults> {
self.swap.as_instrument_results()
}
}
pub type FixedVsFloatingSwapEngine =
GenericEngine<FixedVsFloatingSwapArguments, FixedVsFloatingSwapResults>;
pub struct FixedVsFloatingSwap {
swap: Swap,
swap_type: SwapType,
fixed_nominals: Vec<Real>,
fixed_schedule: Schedule,
fixed_rate: Rate,
fixed_day_count: DayCounter,
floating_nominals: Vec<Real>,
floating_schedule: Schedule,
ibor_index: Shared<IborIndex>,
spread: Spread,
floating_day_count: DayCounter,
payment_convention: BusinessDayConvention,
floating_arguments: FloatingArgumentsFn,
fair_rate: Option<Rate>,
fair_spread: Option<Spread>,
constant_nominals: bool,
same_nominals: bool,
}
impl FixedVsFloatingSwap {
#[allow(clippy::too_many_arguments)]
pub fn new(
swap_type: SwapType,
fixed_nominals: Vec<Real>,
fixed_schedule: Schedule,
fixed_rate: Rate,
fixed_day_count: Option<DayCounter>,
floating_nominals: Vec<Real>,
floating_schedule: Schedule,
ibor_index: Shared<IborIndex>,
spread: Spread,
floating_day_count: DayCounter,
payment_convention: Option<BusinessDayConvention>,
payment_lag: Integer,
payment_calendar: Option<Calendar>,
floating_leg: Leg,
floating_arguments: FloatingArgumentsFn,
settings: Shared<Settings<Date>>,
) -> QlResult<FixedVsFloatingSwap> {
let fixed_day_count = fixed_day_count.unwrap_or_else(|| ibor_index.day_counter().clone());
let payment_convention =
payment_convention.unwrap_or_else(|| floating_schedule.business_day_convention());
let payment_calendar =
payment_calendar.unwrap_or_else(|| fixed_schedule.calendar().clone());
let fixed_leg = FixedRateLeg::new(fixed_schedule.clone())
.with_notionals(fixed_nominals.clone())
.with_coupon_rates(
vec![fixed_rate],
fixed_day_count.clone(),
Compounding::Simple,
Frequency::Annual,
)?
.with_payment_adjustment(payment_convention)
.with_payment_lag(payment_lag)
.with_payment_calendar(payment_calendar)
.build()?;
let payer = match swap_type {
SwapType::Payer => vec![true, false],
SwapType::Receiver => vec![false, true],
};
let same_nominals = fixed_nominals == floating_nominals;
let constant_nominals = same_nominals
&& match fixed_nominals.first() {
Some(&front) => fixed_nominals.iter().all(|&x| x == front),
None => true,
};
let swap = Swap::new(vec![fixed_leg, floating_leg], payer, settings)?;
Ok(FixedVsFloatingSwap {
swap,
swap_type,
fixed_nominals,
fixed_schedule,
fixed_rate,
fixed_day_count,
floating_nominals,
floating_schedule,
ibor_index,
spread,
floating_day_count,
payment_convention,
floating_arguments,
fair_rate: None,
fair_spread: None,
constant_nominals,
same_nominals,
})
}
pub fn swap_type(&self) -> SwapType {
self.swap_type
}
pub fn nominal(&self) -> QlResult<Real> {
require!(self.constant_nominals, "nominal is not constant");
Ok(self.fixed_nominals[0])
}
pub fn nominals(&self) -> QlResult<&[Real]> {
require!(
self.same_nominals,
"different nominals on fixed and floating leg"
);
Ok(&self.fixed_nominals)
}
pub fn fixed_nominals(&self) -> &[Real] {
&self.fixed_nominals
}
pub fn fixed_schedule(&self) -> &Schedule {
&self.fixed_schedule
}
pub fn fixed_rate(&self) -> Rate {
self.fixed_rate
}
pub fn fixed_day_count(&self) -> &DayCounter {
&self.fixed_day_count
}
pub fn floating_nominals(&self) -> &[Real] {
&self.floating_nominals
}
pub fn floating_schedule(&self) -> &Schedule {
&self.floating_schedule
}
pub fn ibor_index(&self) -> &Shared<IborIndex> {
&self.ibor_index
}
pub fn spread(&self) -> Spread {
self.spread
}
pub fn floating_day_count(&self) -> &DayCounter {
&self.floating_day_count
}
pub fn payment_convention(&self) -> BusinessDayConvention {
self.payment_convention
}
pub fn maturity_date(&self) -> QlResult<Date> {
self.swap.maturity_date()
}
pub fn fixed_leg(&self) -> &Leg {
&self.swap.legs()[0]
}
pub fn floating_leg(&self) -> &Leg {
&self.swap.legs()[1]
}
pub fn fixed_leg_bps(&mut self) -> QlResult<Real> {
self.swap.leg_bps(0)
}
pub fn fixed_leg_npv(&mut self) -> QlResult<Real> {
self.swap.leg_npv(0)
}
pub fn floating_leg_bps(&mut self) -> QlResult<Real> {
self.swap.leg_bps(1)
}
pub fn floating_leg_npv(&mut self) -> QlResult<Real> {
self.swap.leg_npv(1)
}
pub fn fair_rate(&mut self) -> QlResult<Rate> {
self.calculate()?;
let Some(value) = self.fair_rate else {
fail!("result not available");
};
Ok(value)
}
pub fn fair_spread(&mut self) -> QlResult<Spread> {
self.calculate()?;
let Some(value) = self.fair_spread else {
fail!("result not available");
};
Ok(value)
}
}
impl Instrument for FixedVsFloatingSwap {
fn base(&self) -> &InstrumentBase {
self.swap.base()
}
fn base_mut(&mut self) -> &mut InstrumentBase {
self.swap.base_mut()
}
fn is_expired(&self) -> QlResult<bool> {
self.swap.is_expired()
}
fn setup_expired(&mut self) {
self.swap.setup_expired();
self.fair_rate = None;
self.fair_spread = None;
}
fn setup_arguments(&self, arguments: &mut dyn Arguments) -> QlResult<()> {
if let Some(args) = (arguments as &mut dyn Any).downcast_mut::<SwapArguments>() {
return self.swap.setup_arguments(args);
}
let Some(args) = (arguments as &mut dyn Any).downcast_mut::<FixedVsFloatingSwapArguments>()
else {
fail!("wrong argument type");
};
self.swap.setup_arguments(&mut args.swap)?;
args.swap_type = Some(self.swap_type);
args.nominal = if self.constant_nominals {
Some(self.nominal()?)
} else {
None
};
let fixed = self.fixed_leg();
let n = fixed.len();
args.fixed_reset_dates = Vec::with_capacity(n);
args.fixed_pay_dates = Vec::with_capacity(n);
args.fixed_nominals = Vec::with_capacity(n);
args.fixed_coupons = Vec::with_capacity(n);
for flow in fixed {
let Some(coupon) = flow.as_coupon() else {
fail!("non-coupon flow on the fixed leg");
};
args.fixed_pay_dates.push(flow.date());
args.fixed_reset_dates.push(coupon.accrual_start_date());
args.fixed_coupons.push(flow.amount()?);
args.fixed_nominals.push(coupon.nominal());
}
(self.floating_arguments)(self, args)
}
fn fetch_results(&mut self, results: &dyn Results) -> QlResult<()> {
let any = results as &dyn Any;
let (swap_results, mut fair_rate, mut fair_spread) =
if let Some(bundle) = any.downcast_ref::<FixedVsFloatingSwapResults>() {
(&bundle.swap, bundle.fair_rate, bundle.fair_spread)
} else if let Some(bundle) = any.downcast_ref::<SwapResults>() {
(bundle, None, None)
} else {
fail!("wrong result type");
};
self.swap.fetch_results(swap_results)?;
let npv = swap_results.instrument.value;
if fair_rate.is_none()
&& let (Some(&bps), Some(npv)) = (swap_results.leg_bps.first(), npv)
{
fair_rate = Some(self.fixed_rate - npv / (bps / BASIS_POINT));
}
if fair_spread.is_none()
&& let (Some(&bps), Some(npv)) = (swap_results.leg_bps.get(1), npv)
{
fair_spread = Some(self.spread - npv / (bps / BASIS_POINT));
}
self.fair_rate = fair_rate;
self.fair_spread = fair_spread;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cashflow::CashFlow;
use crate::cashflows::SimpleCashFlow;
use crate::handle::Handle;
use crate::indexes::ibor::Euribor;
use crate::instruments::swap::SwapEngine;
use crate::patterns::observable::{AsObservable, Observable};
use crate::pricingengine::PricingEngine;
use crate::shared::{SharedMut, shared, shared_mut};
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::schedule::MakeSchedule;
fn settings_today() -> Shared<Settings<Date>> {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(Date::new(7, Month::July, 2026));
settings
}
fn euribor(settings: &Shared<Settings<Date>>) -> Shared<IborIndex> {
shared(Euribor::three_months(
Handle::<dyn YieldTermStructure>::empty(),
Shared::clone(settings),
))
}
fn fixed_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 floating_stub_leg() -> Leg {
vec![
shared(SimpleCashFlow::new(1.0, Date::new(7, Month::July, 2028)).unwrap())
as Shared<dyn CashFlow>,
]
}
fn make_swap(swap_type: SwapType, floating_nominals: Vec<Real>) -> FixedVsFloatingSwap {
make_swap_with_hook(swap_type, floating_nominals, Box::new(|_, _| Ok(())))
}
fn make_swap_with_hook(
swap_type: SwapType,
floating_nominals: Vec<Real>,
floating_arguments: FloatingArgumentsFn,
) -> FixedVsFloatingSwap {
let settings = settings_today();
let index = euribor(&settings);
FixedVsFloatingSwap::new(
swap_type,
vec![100.0],
fixed_schedule(),
0.05,
Some(Actual360::new()),
floating_nominals,
fixed_schedule(),
index,
0.001,
Actual360::new(),
None,
0,
None,
floating_stub_leg(),
floating_arguments,
settings,
)
.unwrap()
}
struct StubSwapEngine {
base: SwapEngine,
npv: Real,
leg_npv: Vec<Real>,
leg_bps: Vec<Real>,
}
impl AsObservable for StubSwapEngine {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl PricingEngine for StubSwapEngine {
fn arguments_mut(&mut self) -> &mut dyn Arguments {
self.base.arguments_mut()
}
fn results(&self) -> &dyn Results {
self.base.results()
}
fn reset(&mut self) {
self.base.reset();
}
fn calculate(&mut self) -> QlResult<()> {
let results = self.base.results_mut();
results.instrument.value = Some(self.npv);
results.leg_npv = self.leg_npv.clone();
results.leg_bps = self.leg_bps.clone();
Ok(())
}
}
fn swap_engine(npv: Real, leg_npv: Vec<Real>, leg_bps: Vec<Real>) -> SharedMut<StubSwapEngine> {
shared_mut(StubSwapEngine {
base: SwapEngine::new(SwapArguments::default(), SwapResults::default()),
npv,
leg_npv,
leg_bps,
})
}
struct StubFvfEngine {
base: FixedVsFloatingSwapEngine,
npv: Real,
leg_bps: Vec<Real>,
fair_rate: Rate,
fair_spread: Spread,
}
impl AsObservable for StubFvfEngine {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl PricingEngine for StubFvfEngine {
fn arguments_mut(&mut self) -> &mut dyn Arguments {
self.base.arguments_mut()
}
fn results(&self) -> &dyn Results {
self.base.results()
}
fn reset(&mut self) {
self.base.reset();
}
fn calculate(&mut self) -> QlResult<()> {
let results = self.base.results_mut();
results.swap.instrument.value = Some(self.npv);
results.swap.leg_bps = self.leg_bps.clone();
results.fair_rate = Some(self.fair_rate);
results.fair_spread = Some(self.fair_spread);
Ok(())
}
}
#[test]
fn the_base_is_built_fixed_then_floating() {
let swap = make_swap(SwapType::Receiver, vec![100.0]);
assert_eq!(swap.fixed_leg().len(), 2, "two annual fixed coupons");
assert_eq!(swap.floating_leg().len(), 1, "the supplied floating stub");
assert_eq!(
swap.floating_leg()[0].date(),
Date::new(7, Month::July, 2028)
);
}
#[test]
fn payer_type_maps_the_two_leg_multipliers() {
let mut receiver = SwapArguments::default();
make_swap(SwapType::Receiver, vec![100.0])
.setup_arguments(&mut receiver)
.unwrap();
assert_eq!(
receiver.payer,
vec![1.0, -1.0],
"receiver: +fixed, -floating"
);
let mut payer = SwapArguments::default();
make_swap(SwapType::Payer, vec![100.0])
.setup_arguments(&mut payer)
.unwrap();
assert_eq!(payer.payer, vec![-1.0, 1.0], "payer: -fixed, +floating");
}
#[test]
fn fair_values_fall_back_from_npv_and_leg_bps() {
let mut swap = make_swap(SwapType::Receiver, vec![100.0]);
swap.base_mut()
.set_pricing_engine(swap_engine(2.0, vec![-98.0, 100.0], vec![-1.0, 2.0]));
assert_eq!(swap.fair_rate().unwrap(), 0.05 - 2.0 / (-1.0 / BASIS_POINT));
assert_eq!(
swap.fair_spread().unwrap(),
0.001 - 2.0 / (2.0 / BASIS_POINT)
);
}
#[test]
fn absent_leg_bps_leaves_fair_values_unavailable() {
let mut swap = make_swap(SwapType::Receiver, vec![100.0]);
swap.base_mut()
.set_pricing_engine(swap_engine(2.0, vec![], vec![]));
assert_eq!(
swap.fair_rate().unwrap_err().message(),
"result not available"
);
assert_eq!(
swap.fair_spread().unwrap_err().message(),
"result not available"
);
}
#[test]
fn specialised_engine_fair_values_are_used_directly() {
let mut swap = make_swap(SwapType::Receiver, vec![100.0]);
swap.base_mut()
.set_pricing_engine(shared_mut(StubFvfEngine {
base: FixedVsFloatingSwapEngine::new(
FixedVsFloatingSwapArguments::default(),
FixedVsFloatingSwapResults::default(),
),
npv: 2.0,
leg_bps: vec![-1.0, 2.0],
fair_rate: 0.09,
fair_spread: 0.007,
}));
assert_eq!(swap.fair_rate().unwrap(), 0.09);
assert_eq!(swap.fair_spread().unwrap(), 0.007);
}
#[test]
fn setup_arguments_fills_fixed_vectors_and_calls_the_floating_hook() {
let marker = Date::new(1, Month::January, 2030);
let swap = make_swap_with_hook(
SwapType::Receiver,
vec![100.0],
Box::new(move |_swap, args| {
args.floating_pay_dates.push(marker);
Ok(())
}),
);
let mut args = FixedVsFloatingSwapArguments::default();
swap.setup_arguments(&mut args).unwrap();
assert_eq!(args.swap_type, Some(SwapType::Receiver));
assert_eq!(args.nominal, Some(100.0));
assert_eq!(args.swap.legs.len(), 2);
assert_eq!(args.fixed_pay_dates.len(), 2);
assert_eq!(args.fixed_reset_dates.len(), 2);
assert_eq!(args.fixed_coupons.len(), 2);
assert_eq!(args.fixed_nominals, vec![100.0, 100.0]);
assert_eq!(args.floating_pay_dates, vec![marker], "the hook ran");
}
#[test]
fn nominal_accessors_gate_on_constant_and_matching_nominals() {
let same = make_swap(SwapType::Receiver, vec![100.0]);
assert_eq!(same.nominal().unwrap(), 100.0);
assert_eq!(same.nominals().unwrap(), &[100.0]);
let different = make_swap(SwapType::Receiver, vec![200.0]);
assert_eq!(
different.nominal().unwrap_err().message(),
"nominal is not constant"
);
assert_eq!(
different.nominals().unwrap_err().message(),
"different nominals on fixed and floating leg"
);
}
#[test]
fn leg_bps_and_npv_accessors_read_the_swap_results() {
let mut swap = make_swap(SwapType::Receiver, vec![100.0]);
swap.base_mut()
.set_pricing_engine(swap_engine(2.0, vec![-98.0, 100.0], vec![-1.0, 2.0]));
assert_eq!(swap.fixed_leg_npv().unwrap(), -98.0);
assert_eq!(swap.floating_leg_npv().unwrap(), 100.0);
assert_eq!(swap.fixed_leg_bps().unwrap(), -1.0);
assert_eq!(swap.floating_leg_bps().unwrap(), 2.0);
}
#[test]
fn an_expired_swap_has_no_fair_values() {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(Date::new(8, Month::July, 2030));
let index = euribor(&settings);
let mut swap = FixedVsFloatingSwap::new(
SwapType::Receiver,
vec![100.0],
fixed_schedule(),
0.05,
Some(Actual360::new()),
vec![100.0],
fixed_schedule(),
index,
0.001,
Actual360::new(),
None,
0,
None,
floating_stub_leg(),
Box::new(|_, _| Ok(())),
settings,
)
.unwrap();
assert_eq!(swap.npv().unwrap(), 0.0);
assert_eq!(
swap.fair_rate().unwrap_err().message(),
"result not available"
);
}
#[test]
fn arguments_validate_rejects_vector_length_mismatches() {
let mut args = FixedVsFloatingSwapArguments {
fixed_nominals: vec![100.0],
fixed_pay_dates: vec![Date::new(7, Month::July, 2028)],
fixed_reset_dates: vec![Date::new(7, Month::July, 2027)],
fixed_coupons: vec![],
..FixedVsFloatingSwapArguments::default()
};
assert_eq!(
args.validate().unwrap_err().message(),
"number of fixed payment dates different from number of fixed coupon amounts"
);
args.fixed_coupons = vec![5.0];
assert!(args.validate().is_ok(), "matched lengths validate");
}
}