use super::couponpricer::FloatingRateCouponPricer;
use super::floatingratecoupon::FloatingRateCoupon;
use crate::errors::QlResult;
use crate::indexes::iborindex::OvernightIndex;
use crate::indexes::index::Index;
use crate::indexes::interestrateindex::InterestRateIndex;
use crate::patterns::observable::{AsObservable, Observable};
use crate::shared::Shared;
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::date::Date;
use crate::types::{Rate, Real, Spread, Time};
use crate::{fail, require};
pub struct OvernightSchedule {
pub(crate) value_dates: Vec<Date>,
pub(crate) interest_dates: Vec<Date>,
pub(crate) fixing_dates: Vec<Date>,
pub(crate) dt: Vec<Time>,
}
impl OvernightSchedule {
pub(crate) fn new(
index: &OvernightIndex,
start_date: Date,
end_date: Date,
) -> QlResult<OvernightSchedule> {
let fixing_calendar = index.fixing_calendar();
let value_dates = fixing_calendar.business_day_list(
fixing_calendar.adjust(start_date, BusinessDayConvention::Preceding),
fixing_calendar.adjust(end_date, BusinessDayConvention::Following),
);
require!(value_dates.len() >= 2, "degenerate schedule");
let n = value_dates.len() - 1;
let mut interest_dates = value_dates.clone();
interest_dates[0] = start_date;
interest_dates[n] = end_date;
let fixing_dates = value_dates[..n].to_vec();
let day_counter = index.day_counter();
let dt = (0..n)
.map(|i| day_counter.year_fraction(interest_dates[i], interest_dates[i + 1]))
.collect();
Ok(OvernightSchedule {
value_dates,
interest_dates,
fixing_dates,
dt,
})
}
fn accrual_end(&self) -> Date {
*self
.interest_dates
.last()
.expect("an overnight schedule always has at least two interest dates")
}
}
fn determine_number_of_fixings(interest_dates: &[Date], date: Date) -> usize {
let searchable = &interest_dates[..interest_dates.len() - 1];
searchable.partition_point(|&d| d < date)
}
pub struct CompoundingOvernightIndexedCouponPricer {
index: Shared<OvernightIndex>,
schedule: Shared<OvernightSchedule>,
gearing: Real,
spread: Spread,
compound_spread_daily: bool,
observable: Observable,
}
impl CompoundingOvernightIndexedCouponPricer {
pub(crate) fn new(
index: Shared<OvernightIndex>,
schedule: Shared<OvernightSchedule>,
gearing: Real,
spread: Spread,
compound_spread_daily: bool,
) -> CompoundingOvernightIndexedCouponPricer {
CompoundingOvernightIndexedCouponPricer {
index,
schedule,
gearing,
spread,
compound_spread_daily,
observable: Observable::new(),
}
}
pub fn average_rate(&self, date: Date) -> QlResult<Rate> {
Ok(self.compute(date)?.0)
}
pub fn effective_spread(&self) -> QlResult<Spread> {
if !self.compound_spread_daily {
return Ok(self.spread);
}
Ok(self.compute(self.schedule.accrual_end())?.1)
}
pub fn effective_index_fixing(&self) -> QlResult<Rate> {
Ok(self.compute(self.schedule.accrual_end())?.2)
}
fn compute(&self, date: Date) -> QlResult<(Rate, Spread, Rate)> {
let index = &self.index;
let today = match index.settings().evaluation_date() {
Some(today) => today,
None => fail!("no evaluation date set: an overnight coupon needs a reference date"),
};
let schedule = &self.schedule;
let day_counter = index.day_counter();
let coupon_spread = self.spread;
let compound_spread_daily = self.compound_spread_daily;
let n = determine_number_of_fixings(&schedule.interest_dates, date);
let growth_factor = |fixing: Rate, idx: usize| -> (Real, Real) {
let span = if date >= schedule.interest_dates[idx + 1] {
schedule.dt[idx]
} else {
day_counter.year_fraction(schedule.interest_dates[idx], date)
};
let gf = 1.0 + fixing * span;
let gf_spread = if compound_spread_daily {
gf + coupon_spread * span
} else {
gf
};
(gf, gf_spread)
};
let mut compound_factor = 1.0;
let mut compound_factor_without_spread = 1.0;
let mut i = 0;
while i < n && schedule.fixing_dates[i] < today {
let fixing = match index.past_fixing(schedule.fixing_dates[i])? {
Some(fixing) => fixing,
None => fail!(
"Missing {} fixing for {:?}",
index.name(),
schedule.fixing_dates[i]
),
};
let (gf, gf_spread) = growth_factor(fixing, i);
compound_factor_without_spread *= gf;
compound_factor *= gf_spread;
i += 1;
}
if i < n
&& schedule.fixing_dates[i] == today
&& let Some(fixing) = index.past_fixing(schedule.fixing_dates[i])?
{
let (gf, gf_spread) = growth_factor(fixing, i);
compound_factor_without_spread *= gf;
compound_factor *= gf_spread;
i += 1;
}
while i < n {
let fixing = index.fixing(schedule.fixing_dates[i], false)?;
let (gf, gf_spread) = growth_factor(fixing, i);
compound_factor_without_spread *= gf;
compound_factor *= gf_spread;
i += 1;
}
let rate_accrual_end = date.min(schedule.interest_dates[n]);
let tau = day_counter.year_fraction(schedule.interest_dates[0], rate_accrual_end);
let rate = (compound_factor - 1.0) / tau;
let mut swaplet_rate = self.gearing * rate;
let effective_spread;
let effective_index_fixing;
if !compound_spread_daily {
swaplet_rate += coupon_spread;
effective_spread = coupon_spread;
effective_index_fixing = rate;
} else {
effective_spread = rate - (compound_factor_without_spread - 1.0) / tau;
effective_index_fixing = rate - effective_spread;
}
Ok((swaplet_rate, effective_spread, effective_index_fixing))
}
}
impl AsObservable for CompoundingOvernightIndexedCouponPricer {
fn observable(&self) -> &Observable {
&self.observable
}
}
impl FloatingRateCouponPricer for CompoundingOvernightIndexedCouponPricer {
fn initialize(&mut self, _coupon: &FloatingRateCoupon) {}
fn swaplet_rate(&self) -> QlResult<Rate> {
Ok(self.compute(self.schedule.accrual_end())?.0)
}
fn swaplet_rate_for(&self, _index_fixing: QlResult<Rate>) -> QlResult<Rate> {
fail!(
"swaplet_rate_for not applicable: the overnight compounding pricer reads the whole daily schedule"
)
}
fn caplet_rate(&self, _effective_cap: Rate, _forward: QlResult<Rate>) -> QlResult<Rate> {
fail!("caplet rate not ported: overnight cap/floor slice")
}
fn floorlet_rate(&self, _effective_floor: Rate, _forward: QlResult<Rate>) -> QlResult<Rate> {
fail!("floorlet rate not ported: overnight cap/floor slice")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::handle::Handle;
use crate::indexes::ibor::Sofr;
use crate::settings::Settings;
use crate::shared::{shared, shared_mut};
use crate::time::date::Month;
fn sofr() -> Shared<OvernightIndex> {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(Date::new(23, Month::November, 2021));
shared(Sofr::new(Handle::empty(), settings))
}
fn schedule(index: &OvernightIndex) -> Shared<OvernightSchedule> {
shared(
OvernightSchedule::new(
index,
Date::new(18, Month::October, 2021),
Date::new(18, Month::November, 2021),
)
.unwrap(),
)
}
#[test]
fn the_schedule_has_consistent_lengths_and_endpoints() {
let index = sofr();
let start = Date::new(18, Month::October, 2021);
let end = Date::new(18, Month::November, 2021);
let schedule = OvernightSchedule::new(&index, start, end).unwrap();
assert!(schedule.value_dates.len() >= 2);
assert_eq!(schedule.fixing_dates.len(), schedule.value_dates.len() - 1);
assert_eq!(schedule.dt.len(), schedule.fixing_dates.len());
assert_eq!(schedule.interest_dates.len(), schedule.value_dates.len());
assert_eq!(*schedule.interest_dates.first().unwrap(), start);
assert_eq!(*schedule.interest_dates.last().unwrap(), end);
}
#[test]
fn a_degenerate_schedule_is_refused() {
let index = sofr();
let day = Date::new(18, Month::October, 2021);
assert!(OvernightSchedule::new(&index, day, day).is_err());
}
#[test]
fn the_pricer_refuses_the_unported_entry_points() {
let index = sofr();
let schedule = schedule(&index);
let pricer = shared_mut(CompoundingOvernightIndexedCouponPricer::new(
index, schedule, 1.0, 0.0, false,
));
let pricer = pricer.borrow();
assert!(pricer.caplet_rate(0.03, Ok(0.01)).is_err());
assert!(pricer.floorlet_rate(0.01, Ok(0.01)).is_err());
assert!(pricer.swaplet_rate_for(Ok(0.01)).is_err());
}
}