use std::any::Any;
use crate::cashflow::{CashFlow, Leg};
use crate::cashflows::{CashFlows, Coupon, IborCoupon};
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>>,
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,
cap_rates,
floor_rates,
settings,
})
}
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 coupons(&self) -> &[Shared<IborCoupon>] {
&self.coupons
}
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>)
.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.coupons.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();
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
);
for (i, coupon) in self.coupons.iter().enumerate() {
let spread = coupon.spread();
let gearing = coupon.gearing();
let end_date = coupon.date();
args.start_dates.push(coupon.accrual_start_date());
args.fixing_dates.push(coupon.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;
use crate::handle::Handle;
use crate::indexes::ibor::Euribor;
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());
}
}