use std::any::Any;
use crate::cashflow::{CashFlow, Leg};
use crate::cashflows::{CashFlows, Coupon, IborCoupon, OvernightIndexedCoupon};
use crate::errors::QlResult;
use crate::event::Event;
use crate::instrument::{Instrument, InstrumentBase};
use crate::patterns::observable::AsObservable;
use crate::pricingengine::Arguments;
use crate::settings::Settings;
use crate::shared::Shared;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::date::Date;
use crate::types::{Rate, Real, Time};
use crate::{fail, require};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CapFloorType {
Cap,
Floor,
Collar,
}
#[derive(Default)]
pub struct CapFloorArguments {
pub cap_floor_type: Option<CapFloorType>,
pub start_dates: Vec<Date>,
pub fixing_dates: Vec<Date>,
pub end_dates: Vec<Date>,
pub accrual_times: Vec<Time>,
pub cap_rates: Vec<Option<Rate>>,
pub floor_rates: Vec<Option<Rate>>,
pub forwards: Vec<Option<Rate>>,
pub gearings: Vec<Real>,
pub nominals: Vec<Real>,
}
impl Arguments for CapFloorArguments {
fn validate(&self) -> QlResult<()> {
let n = self.end_dates.len();
require!(self.cap_floor_type.is_some(), "cap/floor type not set");
require!(self.start_dates.len() == n, "start-date count mismatch");
require!(self.fixing_dates.len() == n, "fixing-date count mismatch");
require!(self.accrual_times.len() == n, "accrual-time count mismatch");
require!(self.cap_rates.len() == n, "cap-rate count mismatch");
require!(self.floor_rates.len() == n, "floor-rate count mismatch");
require!(self.forwards.len() == n, "forward count mismatch");
require!(self.gearings.len() == n, "gearing count mismatch");
require!(self.nominals.len() == n, "nominal count mismatch");
Ok(())
}
}
pub struct CapFloor {
base: InstrumentBase,
cap_floor_type: CapFloorType,
coupons: Vec<Shared<IborCoupon>>,
overnight_coupons: Vec<Shared<OvernightIndexedCoupon>>,
cap_rates: Vec<Rate>,
floor_rates: Vec<Rate>,
settings: Shared<Settings<Date>>,
}
impl CapFloor {
pub fn new(
cap_floor_type: CapFloorType,
coupons: Vec<Shared<IborCoupon>>,
mut cap_rates: Vec<Rate>,
mut floor_rates: Vec<Rate>,
settings: Shared<Settings<Date>>,
) -> QlResult<CapFloor> {
let n = coupons.len();
if matches!(cap_floor_type, CapFloorType::Cap | CapFloorType::Collar) {
require!(!cap_rates.is_empty(), "no cap rates given");
while cap_rates.len() < n {
cap_rates.push(*cap_rates.last().expect("non-empty"));
}
}
if matches!(cap_floor_type, CapFloorType::Floor | CapFloorType::Collar) {
require!(!floor_rates.is_empty(), "no floor rates given");
while floor_rates.len() < n {
floor_rates.push(*floor_rates.last().expect("non-empty"));
}
}
let base = InstrumentBase::new();
for coupon in &coupons {
base.register_with(coupon.observable());
}
settings.register_eval_date_observer(&base.observer());
Ok(CapFloor {
base,
cap_floor_type,
coupons,
overnight_coupons: Vec::new(),
cap_rates,
floor_rates,
settings,
})
}
pub fn from_overnight(
kind: CapFloorType,
coupons: Vec<Shared<OvernightIndexedCoupon>>,
cap_rates: Vec<Rate>,
floor_rates: Vec<Rate>,
settings: Shared<Settings<Date>>,
) -> QlResult<Self> {
let mut result = Self::new(kind, Vec::new(), cap_rates, floor_rates, settings)?;
if result.cap_rates.len() < coupons.len()
&& let Some(&last) = result.cap_rates.last()
{
result.cap_rates.resize(coupons.len(), last);
}
if result.floor_rates.len() < coupons.len()
&& let Some(&last) = result.floor_rates.last()
{
result.floor_rates.resize(coupons.len(), last);
}
for coupon in &coupons {
result.base.register_with(coupon.observable());
}
result.overnight_coupons = coupons;
Ok(result)
}
pub fn overnight_coupons(&self) -> &[Shared<OvernightIndexedCoupon>] {
&self.overnight_coupons
}
pub fn cap(
coupons: Vec<Shared<IborCoupon>>,
strikes: Vec<Rate>,
settings: Shared<Settings<Date>>,
) -> QlResult<CapFloor> {
CapFloor::new(CapFloorType::Cap, coupons, strikes, Vec::new(), settings)
}
pub fn floor(
coupons: Vec<Shared<IborCoupon>>,
strikes: Vec<Rate>,
settings: Shared<Settings<Date>>,
) -> QlResult<CapFloor> {
CapFloor::new(CapFloorType::Floor, coupons, Vec::new(), strikes, settings)
}
pub fn collar(
coupons: Vec<Shared<IborCoupon>>,
cap_rates: Vec<Rate>,
floor_rates: Vec<Rate>,
settings: Shared<Settings<Date>>,
) -> QlResult<CapFloor> {
CapFloor::new(
CapFloorType::Collar,
coupons,
cap_rates,
floor_rates,
settings,
)
}
pub fn cap_floor_type(&self) -> CapFloorType {
self.cap_floor_type
}
pub fn cap_rates(&self) -> &[Rate] {
&self.cap_rates
}
pub fn floor_rates(&self) -> &[Rate] {
&self.floor_rates
}
pub fn coupon_count(&self) -> usize {
self.coupons.len() + self.overnight_coupons.len()
}
pub fn optionlet(&self, index: usize) -> QlResult<Self> {
require!(
index < self.coupon_count(),
"optionlet index {index} does not exist, only {} coupons",
self.coupon_count()
);
let cap_rates = if matches!(
self.cap_floor_type,
CapFloorType::Cap | CapFloorType::Collar
) {
vec![self.cap_rates[index]]
} else {
Vec::new()
};
let floor_rates = if matches!(
self.cap_floor_type,
CapFloorType::Floor | CapFloorType::Collar
) {
vec![self.floor_rates[index]]
} else {
Vec::new()
};
if let Some(coupon) = self.coupons.get(index) {
Self::new(
self.cap_floor_type,
vec![Shared::clone(coupon)],
cap_rates,
floor_rates,
Shared::clone(&self.settings),
)
} else {
Self::from_overnight(
self.cap_floor_type,
vec![Shared::clone(
&self.overnight_coupons[index - self.coupons.len()],
)],
cap_rates,
floor_rates,
Shared::clone(&self.settings),
)
}
}
pub fn coupons(&self) -> &[Shared<IborCoupon>] {
&self.coupons
}
pub fn last_floating_rate_coupon(&self) -> Option<&Shared<IborCoupon>> {
self.coupons.last()
}
pub fn last_overnight_coupon(&self) -> Option<&Shared<OvernightIndexedCoupon>> {
self.overnight_coupons.last()
}
pub fn start_date(&self) -> QlResult<Date> {
CashFlows::start_date(&self.cash_flows())
}
pub fn maturity_date(&self) -> QlResult<Date> {
CashFlows::maturity_date(&self.cash_flows())
}
pub fn atm_rate(&self, discount_curve: &dyn YieldTermStructure) -> QlResult<Rate> {
let reference = discount_curve.reference_date()?;
CashFlows::atm_rate(
&self.cash_flows(),
discount_curve,
&self.settings,
Some(false),
Some(reference),
None,
None,
)
}
fn cash_flows(&self) -> Leg {
self.coupons
.iter()
.map(|coupon| Shared::clone(coupon) as Shared<dyn CashFlow>)
.chain(
self.overnight_coupons
.iter()
.map(|coupon| Shared::clone(coupon) as Shared<dyn CashFlow>),
)
.collect()
}
}
impl Instrument for CapFloor {
fn base(&self) -> &InstrumentBase {
&self.base
}
fn base_mut(&mut self) -> &mut InstrumentBase {
&mut self.base
}
fn is_expired(&self) -> QlResult<bool> {
for coupon in self.cash_flows().iter().rev() {
if !coupon.has_occurred(&self.settings, None, None)? {
return Ok(false);
}
}
Ok(true)
}
fn setup_arguments(&self, arguments: &mut dyn Arguments) -> QlResult<()> {
let Some(args) = (arguments as &mut dyn Any).downcast_mut::<CapFloorArguments>() else {
fail!("wrong argument type");
};
let today = match self.settings.evaluation_date() {
Some(today) => today,
None => fail!("no evaluation date set: a cap/floor needs a reference date"),
};
let n = self.coupons.len() + self.overnight_coupons.len();
args.cap_floor_type = Some(self.cap_floor_type);
args.start_dates = Vec::with_capacity(n);
args.fixing_dates = Vec::with_capacity(n);
args.end_dates = Vec::with_capacity(n);
args.accrual_times = Vec::with_capacity(n);
args.cap_rates = Vec::with_capacity(n);
args.floor_rates = Vec::with_capacity(n);
args.forwards = Vec::with_capacity(n);
args.gearings = Vec::with_capacity(n);
args.nominals = Vec::with_capacity(n);
let has_cap = matches!(
self.cap_floor_type,
CapFloorType::Cap | CapFloorType::Collar
);
let has_floor = matches!(
self.cap_floor_type,
CapFloorType::Floor | CapFloorType::Collar
);
let coupons = self
.coupons
.iter()
.map(|c| {
(
c.as_ref() as &dyn Coupon,
c.fixing_date(),
c.spread(),
c.gearing(),
c.date(),
)
})
.chain(self.overnight_coupons.iter().map(|c| {
(
c.as_ref() as &dyn Coupon,
c.fixing_date(),
c.spread(),
c.gearing(),
c.date(),
)
}));
for (i, (coupon, fixing_date, spread, gearing, end_date)) in coupons.enumerate() {
args.start_dates.push(coupon.accrual_start_date());
args.fixing_dates.push(fixing_date);
args.end_dates.push(end_date);
args.accrual_times.push(coupon.accrual_period());
args.nominals.push(coupon.nominal());
args.gearings.push(gearing);
let forward = if end_date >= today {
Some((coupon.rate()? - spread) / gearing)
} else {
None
};
args.forwards.push(forward);
args.cap_rates.push(if has_cap {
Some((self.cap_rates[i] - spread) / gearing)
} else {
None
});
args.floor_rates.push(if has_floor {
Some((self.floor_rates[i] - spread) / gearing)
} else {
None
});
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cashflows::{IborLeg, OvernightLeg};
use crate::handle::Handle;
use crate::indexes::ibor::{Euribor, Sofr};
use crate::shared::shared;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendars::target::Target;
use crate::time::date::{Date, Month};
use crate::time::frequency::Frequency;
use crate::time::schedule::MakeSchedule;
fn settings_on(today: Date) -> Shared<Settings<Date>> {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(today);
settings
}
fn leg(settings: Shared<Settings<Date>>) -> Vec<Shared<IborCoupon>> {
let index = shared(Euribor::six_months(
Handle::<dyn YieldTermStructure>::empty(),
settings,
));
let schedule = MakeSchedule::new()
.from(Date::new(15, Month::January, 2026))
.to(Date::new(15, Month::July, 2027))
.with_frequency(Frequency::Semiannual)
.with_calendar(Target::new())
.with_convention(BusinessDayConvention::ModifiedFollowing)
.build();
IborLeg::new(schedule, index)
.with_notional(100.0)
.coupons()
.unwrap()
}
#[test]
fn a_cap_pads_the_strike_to_the_leg_length() {
let settings = settings_on(Date::new(2, Month::January, 2026));
let coupons = leg(settings.clone());
let n = coupons.len();
let cap = CapFloor::cap(coupons, vec![0.03], settings).unwrap();
assert_eq!(cap.cap_floor_type(), CapFloorType::Cap);
assert_eq!(cap.cap_rates(), vec![0.03; n].as_slice());
assert!(cap.floor_rates().is_empty());
}
#[test]
fn a_collar_keeps_both_padded_strike_vectors() {
let settings = settings_on(Date::new(2, Month::January, 2026));
let coupons = leg(settings.clone());
let n = coupons.len();
let collar = CapFloor::collar(coupons, vec![0.06], vec![0.02], settings).unwrap();
assert_eq!(collar.cap_floor_type(), CapFloorType::Collar);
assert_eq!(collar.cap_rates(), vec![0.06; n].as_slice());
assert_eq!(collar.floor_rates(), vec![0.02; n].as_slice());
}
#[test]
fn a_cap_needs_at_least_one_rate() {
let settings = settings_on(Date::new(2, Month::January, 2026));
let coupons = leg(settings.clone());
let err = CapFloor::cap(coupons, Vec::new(), settings).err().unwrap();
assert_eq!(err.message(), "no cap rates given");
}
#[test]
fn a_floor_needs_at_least_one_rate() {
let settings = settings_on(Date::new(2, Month::January, 2026));
let coupons = leg(settings.clone());
let err = CapFloor::floor(coupons, Vec::new(), settings)
.err()
.unwrap();
assert_eq!(err.message(), "no floor rates given");
}
#[test]
fn setup_arguments_fills_start_dates_with_the_accrual_starts() {
let settings = settings_on(Date::new(2, Month::January, 2028));
let coupons = leg(settings.clone());
let expected: Vec<Date> = coupons.iter().map(|c| c.accrual_start_date()).collect();
let cap = CapFloor::cap(coupons, vec![0.03], settings).unwrap();
let mut args = CapFloorArguments::default();
cap.setup_arguments(&mut args).unwrap();
assert_eq!(args.start_dates, expected);
assert_eq!(args.start_dates.len(), args.end_dates.len());
}
#[test]
fn validate_rejects_a_desynced_start_date_count() {
let settings = settings_on(Date::new(2, Month::January, 2028));
let coupons = leg(settings.clone());
let cap = CapFloor::cap(coupons, vec![0.03], settings).unwrap();
let mut args = CapFloorArguments::default();
cap.setup_arguments(&mut args).unwrap();
args.start_dates.pop();
assert_eq!(
args.validate().err().unwrap().message(),
"start-date count mismatch"
);
}
#[test]
fn a_cap_is_expired_only_once_all_coupons_have_paid() {
let settings = settings_on(Date::new(2, Month::January, 2026));
let coupons = leg(settings.clone());
let cap = CapFloor::cap(coupons, vec![0.03], settings.clone()).unwrap();
assert!(!cap.is_expired().unwrap());
settings.set_evaluation_date(Date::new(15, Month::August, 2027));
assert!(cap.is_expired().unwrap());
}
#[test]
fn last_overnight_coupon_retains_the_trailing_coupon_identity() {
let settings = settings_on(Date::new(2, Month::January, 2026));
let index = shared(Sofr::new(Handle::empty(), settings.clone()));
let schedule = MakeSchedule::new()
.from(Date::new(15, Month::January, 2026))
.to(Date::new(15, Month::October, 2026))
.with_frequency(Frequency::Quarterly)
.with_calendar(Target::new())
.with_convention(BusinessDayConvention::ModifiedFollowing)
.build();
let coupons = OvernightLeg::new(schedule, index)
.with_notional(100.0)
.coupons()
.unwrap();
assert_eq!(coupons.len(), 3);
let cap = CapFloor::from_overnight(
CapFloorType::Cap,
coupons.clone(),
vec![0.03],
Vec::new(),
settings,
)
.unwrap();
let last = cap.last_overnight_coupon().unwrap();
assert!(Shared::ptr_eq(last, &coupons[2]));
assert!(std::ptr::eq(last, &cap.overnight_coupons()[2]));
assert!(!Shared::ptr_eq(last, &coupons[0]));
assert!(cap.last_floating_rate_coupon().is_none());
assert_eq!(last.accrual_end_date(), Date::new(15, Month::October, 2026));
}
#[test]
fn last_overnight_coupon_is_none_on_ibor_and_empty_legs() {
let settings = settings_on(Date::new(2, Month::January, 2026));
let coupons = leg(settings.clone());
let last = coupons.last().unwrap().clone();
let cap = CapFloor::cap(coupons, vec![0.03], settings.clone()).unwrap();
assert!(cap.last_overnight_coupon().is_none());
assert!(Shared::ptr_eq(
cap.last_floating_rate_coupon().unwrap(),
&last
));
let empty = CapFloor::from_overnight(
CapFloorType::Cap,
Vec::new(),
vec![0.03],
Vec::new(),
settings,
)
.unwrap();
assert!(empty.last_overnight_coupon().is_none());
assert!(empty.last_floating_rate_coupon().is_none());
}
}