use super::coupon::{Coupon, CouponBase};
use super::couponpricer::FloatingRateCouponPricer;
use super::iborcoupon::IborCoupon;
use crate::errors::QlResult;
use crate::indexes::iborindex::IborIndex;
use crate::patterns::observable::{AsObservable, Observable};
use crate::shared::{Shared, SharedMut, shared};
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::date::Date;
use crate::time::daycounter::DayCounter;
use crate::types::{Natural, Rate, Real, Spread};
use crate::{fail, require};
pub struct CappedFlooredCoupon {
underlying: Shared<IborCoupon>,
is_capped: bool,
is_floored: bool,
cap: Rate,
floor: Rate,
}
impl CappedFlooredCoupon {
pub fn new(
underlying: Shared<IborCoupon>,
cap: Option<Rate>,
floor: Option<Rate>,
) -> QlResult<CappedFlooredCoupon> {
let mut is_capped = false;
let mut is_floored = false;
let mut cap_value = 0.0;
let mut floor_value = 0.0;
if underlying.gearing() > 0.0 {
if let Some(cap) = cap {
is_capped = true;
cap_value = cap;
}
if let Some(floor) = floor {
is_floored = true;
floor_value = floor;
}
} else {
if let Some(cap) = cap {
is_floored = true;
floor_value = cap;
}
if let Some(floor) = floor {
is_capped = true;
cap_value = floor;
}
}
if let (Some(cap), Some(floor)) = (cap, floor) {
let cap_at_least_floor = cap >= floor;
require!(
cap_at_least_floor,
"cap level ({cap}) less than floor level ({floor})"
);
}
Ok(CappedFlooredCoupon {
underlying,
is_capped,
is_floored,
cap: cap_value,
floor: floor_value,
})
}
pub fn underlying(&self) -> &Shared<IborCoupon> {
&self.underlying
}
pub fn is_capped(&self) -> bool {
self.is_capped
}
pub fn is_floored(&self) -> bool {
self.is_floored
}
fn effective_cap(&self) -> Rate {
(self.cap - self.underlying.spread()) / self.underlying.gearing()
}
fn effective_floor(&self) -> Rate {
(self.floor - self.underlying.spread()) / self.underlying.gearing()
}
pub fn set_pricer(&self, pricer: SharedMut<dyn FloatingRateCouponPricer>) {
self.underlying.set_pricer(pricer);
}
}
impl AsObservable for CappedFlooredCoupon {
fn observable(&self) -> &Observable {
self.underlying.observable()
}
}
impl Coupon for CappedFlooredCoupon {
fn coupon_base(&self) -> &CouponBase {
self.underlying.coupon_base()
}
fn amount(&self) -> QlResult<Real> {
Ok(self.rate()? * self.accrual_period() * self.nominal())
}
fn rate(&self) -> QlResult<Rate> {
let swaplet = self.underlying.rate()?;
let Some(pricer) = self.underlying.pricer() else {
fail!("pricer not set");
};
let forward = self.underlying.index_fixing();
let mut rate = swaplet;
if self.is_floored {
rate += pricer
.borrow()
.floorlet_rate(self.effective_floor(), forward.clone())?;
}
if self.is_capped {
rate -= pricer.borrow().caplet_rate(self.effective_cap(), forward)?;
}
Ok(rate)
}
fn day_counter(&self) -> DayCounter {
self.underlying.day_counter()
}
fn accrued_amount(&self, date: Date) -> QlResult<Real> {
if date <= self.accrual_start_date() || date > self.coupon_base().payment_date() {
Ok(0.0)
} else {
Ok(self.nominal() * self.rate()? * self.accrued_period(date))
}
}
}
pub struct CappedFlooredIborCoupon;
impl CappedFlooredIborCoupon {
#[allow(clippy::too_many_arguments, clippy::new_ret_no_self)]
pub fn new(
payment_date: Date,
nominal: Real,
accrual_start_date: Date,
accrual_end_date: Date,
fixing_days: Option<Natural>,
index: Shared<IborIndex>,
gearing: Real,
spread: Spread,
cap: Option<Rate>,
floor: Option<Rate>,
ref_period_start: Option<Date>,
ref_period_end: Option<Date>,
day_counter: Option<DayCounter>,
is_in_arrears: bool,
ex_coupon_date: Option<Date>,
fixing_convention: BusinessDayConvention,
) -> QlResult<CappedFlooredCoupon> {
let underlying = IborCoupon::new(
payment_date,
nominal,
accrual_start_date,
accrual_end_date,
fixing_days,
index,
gearing,
spread,
ref_period_start,
ref_period_end,
day_counter,
is_in_arrears,
ex_coupon_date,
fixing_convention,
)?;
CappedFlooredCoupon::new(shared(underlying), cap, floor)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::currency::Currency;
use crate::handle::Handle;
use crate::indexes::index::Index;
use crate::settings::Settings;
use crate::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::period::Period;
use crate::time::timeunit::TimeUnit;
fn determined_coupon(gearing: Real, spread: Spread) -> Shared<IborCoupon> {
let today = Date::new(15, Month::June, 2026);
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(today);
let index = shared(IborIndex::new(
"foo".into(),
Period::new(6, TimeUnit::Months),
2,
Currency::eur(),
Target::new(),
BusinessDayConvention::Following,
false,
Actual360::new(),
Handle::<dyn YieldTermStructure>::empty(),
settings,
));
let start = Date::new(3, Month::June, 2026);
let end = Date::new(3, Month::December, 2026);
let coupon = shared(
IborCoupon::new(
end,
100.0,
start,
end,
Some(2),
index.clone(),
gearing,
spread,
None,
None,
None,
false,
None,
BusinessDayConvention::Preceding,
)
.unwrap(),
);
index.add_fixing(coupon.fixing_date(), 0.05).unwrap();
coupon
}
fn pricer() -> SharedMut<dyn FloatingRateCouponPricer> {
shared_mut(super::super::couponpricer::BlackIborCouponPricer::new())
as SharedMut<dyn FloatingRateCouponPricer>
}
#[test]
fn a_collar_clamps_the_determined_rate() {
let coupon =
CappedFlooredCoupon::new(determined_coupon(1.0, 0.0), Some(0.03), Some(0.02)).unwrap();
coupon.set_pricer(pricer());
assert!(coupon.is_capped() && coupon.is_floored());
assert!((coupon.rate().unwrap() - 0.03).abs() < 1e-15);
}
#[test]
fn a_floor_lifts_the_determined_rate() {
let coupon =
CappedFlooredCoupon::new(determined_coupon(1.0, 0.0), None, Some(0.06)).unwrap();
coupon.set_pricer(pricer());
assert!(coupon.is_floored() && !coupon.is_capped());
assert!((coupon.rate().unwrap() - 0.06).abs() < 1e-15);
}
#[test]
fn a_negative_gearing_swaps_cap_and_floor() {
let coupon =
CappedFlooredCoupon::new(determined_coupon(-1.5, 0.0), Some(0.10), None).unwrap();
assert!(coupon.is_floored() && !coupon.is_capped());
}
#[test]
fn set_pricer_installs_the_one_instance_the_rate_path_reads() {
let coupon =
CappedFlooredCoupon::new(determined_coupon(1.0, 0.0), Some(0.03), None).unwrap();
let p1 = pricer();
coupon.set_pricer(p1.clone());
assert!(SharedMut::ptr_eq(
&coupon.underlying().pricer().unwrap(),
&p1
));
let first = coupon.rate().unwrap();
let p2 = pricer();
coupon.set_pricer(p2.clone());
assert!(SharedMut::ptr_eq(
&coupon.underlying().pricer().unwrap(),
&p2
));
assert!((coupon.rate().unwrap() - first).abs() < 1e-15);
}
#[test]
fn a_cap_below_its_floor_is_rejected() {
let err = CappedFlooredCoupon::new(determined_coupon(1.0, 0.0), Some(0.02), Some(0.03))
.err()
.expect("a cap below its floor is an error");
assert!(err.message().contains("less than floor"));
}
}