use super::coupon::{Coupon, CouponBase};
use super::couponpricer::FloatingRateCouponPricer;
use super::floatingratecoupon::FloatingRateCoupon;
use super::overnightindexedcouponpricer::{
CompoundingOvernightIndexedCouponPricer, OvernightSchedule,
};
use super::rateaveraging::RateAveraging;
use crate::errors::QlResult;
use crate::indexes::iborindex::OvernightIndex;
use crate::indexes::index::Index;
use crate::patterns::observable::{AsObservable, Observable};
use crate::require;
use crate::shared::{Shared, SharedMut, shared, shared_mut};
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::date::Date;
use crate::time::daycounter::DayCounter;
use crate::types::{Rate, Real, Spread, Time};
pub struct OvernightIndexedCoupon {
base: FloatingRateCoupon,
overnight_index: Shared<OvernightIndex>,
schedule: Shared<OvernightSchedule>,
averaging_method: RateAveraging,
compound_spread_daily: bool,
pricer: SharedMut<CompoundingOvernightIndexedCouponPricer>,
}
impl OvernightIndexedCoupon {
#[allow(clippy::too_many_arguments)]
pub fn new(
payment_date: Date,
nominal: Real,
start_date: Date,
end_date: Date,
index: Shared<OvernightIndex>,
gearing: Real,
spread: Spread,
ref_period_start: Option<Date>,
ref_period_end: Option<Date>,
day_counter: Option<DayCounter>,
averaging_method: RateAveraging,
compound_spread_daily: bool,
ex_coupon_date: Option<Date>,
) -> QlResult<OvernightIndexedCoupon> {
require!(start_date < end_date, "startDate must be less than endDate");
require!(
payment_date >= end_date,
"Payment date cannot be earlier than accrual end date"
);
require!(
averaging_method == RateAveraging::Compound,
"simple-averaged overnight coupon not ported: only compound averaging"
);
let schedule = shared(OvernightSchedule::new(&index, start_date, end_date)?);
let base = FloatingRateCoupon::new(
payment_date,
nominal,
start_date,
end_date,
None,
index.clone(),
gearing,
spread,
ref_period_start,
ref_period_end,
day_counter,
false,
ex_coupon_date,
BusinessDayConvention::Preceding,
)?;
let pricer = shared_mut(CompoundingOvernightIndexedCouponPricer::new(
index.clone(),
schedule.clone(),
gearing,
spread,
compound_spread_daily,
));
base.set_pricer(pricer.clone() as SharedMut<dyn FloatingRateCouponPricer>);
Ok(OvernightIndexedCoupon {
base,
overnight_index: index,
schedule,
averaging_method,
compound_spread_daily,
pricer,
})
}
pub fn overnight_index(&self) -> &Shared<OvernightIndex> {
&self.overnight_index
}
pub fn fixing_dates(&self) -> &[Date] {
&self.schedule.fixing_dates
}
pub fn value_dates(&self) -> &[Date] {
&self.schedule.value_dates
}
pub fn interest_dates(&self) -> &[Date] {
&self.schedule.interest_dates
}
pub fn dt(&self) -> &[Time] {
&self.schedule.dt
}
pub fn averaging_method(&self) -> RateAveraging {
self.averaging_method
}
pub fn compound_spread_daily(&self) -> bool {
self.compound_spread_daily
}
pub fn spread(&self) -> Spread {
self.base.spread()
}
pub fn fixing_date(&self) -> Date {
*self
.schedule
.fixing_dates
.last()
.expect("an overnight schedule always has at least one fixing date")
}
pub fn index_fixings(&self) -> QlResult<Vec<Rate>> {
self.schedule
.fixing_dates
.iter()
.map(|&date| self.overnight_index.fixing(date, false))
.collect()
}
pub fn effective_spread(&self) -> QlResult<Spread> {
self.pricer.borrow().effective_spread()
}
pub fn effective_index_fixing(&self) -> QlResult<Rate> {
self.pricer.borrow().effective_index_fixing()
}
fn average_rate(&self, date: Date) -> QlResult<Rate> {
self.pricer.borrow().average_rate(date)
}
}
impl AsObservable for OvernightIndexedCoupon {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl Coupon for OvernightIndexedCoupon {
fn coupon_base(&self) -> &CouponBase {
self.base.coupon_base()
}
fn amount(&self) -> QlResult<Real> {
Ok(self.rate()? * self.accrual_period() * self.nominal())
}
fn rate(&self) -> QlResult<Rate> {
self.base.rate()
}
fn day_counter(&self) -> DayCounter {
self.base.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 if self.trades_ex_coupon_on(date) {
Ok(self.nominal() * self.average_rate(date)? * self.accrued_period(date))
} else {
let capped = date.min(self.accrual_end_date());
Ok(self.nominal() * self.average_rate(capped)? * self.accrued_period(date))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::handle::RelinkableHandle;
use crate::indexes::ibor::Sofr;
use crate::indexes::index::Index;
use crate::interestrate::Compounding;
use crate::settings::Settings;
use crate::shared::shared;
use crate::termstructures::yields::FlatForward;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::date::Month::{August, December, January, July, June, November, October};
use crate::time::daycounters::actual360::Actual360;
use crate::time::frequency::Frequency;
const NOTIONAL: Real = 10000.0;
fn common_vars(
today: Date,
) -> (
Shared<Settings<Date>>,
RelinkableHandle<dyn YieldTermStructure>,
Shared<OvernightIndex>,
) {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(today);
let curve: RelinkableHandle<dyn YieldTermStructure> = RelinkableHandle::empty();
let sofr = shared(Sofr::new(curve.handle(), settings.clone()));
let past_dates = [
Date::new(21, June, 2019),
Date::new(24, June, 2019),
Date::new(25, June, 2019),
Date::new(26, June, 2019),
Date::new(27, June, 2019),
Date::new(28, June, 2019),
Date::new(1, July, 2019),
Date::new(2, July, 2019),
Date::new(3, July, 2019),
Date::new(5, July, 2019),
Date::new(8, July, 2019),
Date::new(9, July, 2019),
Date::new(10, July, 2019),
Date::new(11, July, 2019),
Date::new(12, July, 2019),
Date::new(15, July, 2019),
Date::new(16, July, 2019),
Date::new(17, July, 2019),
Date::new(18, July, 2019),
Date::new(19, July, 2019),
Date::new(22, July, 2019),
Date::new(23, July, 2019),
Date::new(24, July, 2019),
Date::new(25, July, 2019),
Date::new(26, July, 2019),
Date::new(29, July, 2019),
Date::new(30, July, 2019),
Date::new(31, July, 2019),
Date::new(1, August, 2019),
Date::new(2, August, 2019),
Date::new(5, August, 2019),
Date::new(18, October, 2021),
Date::new(19, October, 2021),
Date::new(20, October, 2021),
Date::new(21, October, 2021),
Date::new(22, October, 2021),
Date::new(25, October, 2021),
Date::new(26, October, 2021),
Date::new(27, October, 2021),
Date::new(28, October, 2021),
Date::new(29, October, 2021),
Date::new(1, November, 2021),
Date::new(2, November, 2021),
Date::new(3, November, 2021),
Date::new(4, November, 2021),
Date::new(5, November, 2021),
Date::new(8, November, 2021),
Date::new(9, November, 2021),
Date::new(10, November, 2021),
Date::new(12, November, 2021),
Date::new(15, November, 2021),
Date::new(16, November, 2021),
Date::new(17, November, 2021),
Date::new(18, November, 2021),
Date::new(19, November, 2021),
Date::new(22, November, 2021),
];
let past_rates = [
0.0237, 0.0239, 0.0241, 0.0243, 0.0242, 0.025, 0.0242, 0.0251, 0.0256, 0.0259, 0.0248,
0.0245, 0.0246, 0.0241, 0.0236, 0.0246, 0.0247, 0.0247, 0.0246, 0.0241, 0.024, 0.024,
0.0241, 0.0242, 0.0241, 0.024, 0.0239, 0.0255, 0.0219, 0.0219, 0.0213, 0.0008, 0.0009,
0.0008, 0.0010, 0.0012, 0.0011, 0.0013, 0.0012, 0.0012, 0.0008, 0.0009, 0.0010, 0.0011,
0.0014, 0.0013, 0.0011, 0.0009, 0.0008, 0.0007, 0.0008, 0.0008, 0.0007, 0.0009, 0.0010,
0.0009,
];
sofr.add_fixings(past_dates.into_iter().zip(past_rates))
.unwrap();
(settings, curve, sofr)
}
fn flat_rate(reference: Date, rate: Rate) -> Shared<dyn YieldTermStructure> {
shared(FlatForward::with_rate(
reference,
rate,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>
}
fn make_coupon(sofr: Shared<OvernightIndex>, start: Date, end: Date) -> OvernightIndexedCoupon {
OvernightIndexedCoupon::new(
end,
NOTIONAL,
start,
end,
sofr,
1.0,
0.0,
None,
None,
None,
RateAveraging::Compound,
false,
None,
)
.unwrap()
}
fn make_spreaded_coupon(
sofr: Shared<OvernightIndex>,
start: Date,
end: Date,
spread: Spread,
compound_spread_daily: bool,
) -> OvernightIndexedCoupon {
OvernightIndexedCoupon::new(
end,
NOTIONAL,
start,
end,
sofr,
1.0,
spread,
None,
None,
None,
RateAveraging::Compound,
compound_spread_daily,
None,
)
.unwrap()
}
#[test]
fn a_past_coupon_compounds_its_recorded_fixings() {
let (_settings, _curve, sofr) = common_vars(Date::new(23, November, 2021));
let coupon = make_coupon(
sofr,
Date::new(18, October, 2021),
Date::new(18, November, 2021),
);
let expected_rate = 0.000987136104;
assert!((coupon.rate().unwrap() - expected_rate).abs() < 1e-12);
let expected_amount = NOTIONAL * expected_rate * 31.0 / 360.0;
assert!((coupon.amount().unwrap() - expected_amount).abs() < 1e-8);
}
#[test]
fn a_past_spreaded_coupon_prices_both_spread_modes() {
let (_settings, _curve, sofr) = common_vars(Date::new(23, November, 2021));
let compounded_daily = make_spreaded_coupon(
sofr.clone(),
Date::new(18, October, 2021),
Date::new(18, November, 2021),
0.0001,
true,
);
let expected_rate = 0.0010871445057780704;
assert!((compounded_daily.rate().unwrap() - expected_rate).abs() < 1e-12);
let expected_amount = NOTIONAL * expected_rate * 31.0 / 360.0;
assert!((compounded_daily.amount().unwrap() - expected_amount).abs() < 1e-8);
let added_after = make_spreaded_coupon(
sofr,
Date::new(18, October, 2021),
Date::new(18, November, 2021),
0.0001,
false,
);
assert!((added_after.rate().unwrap() - 0.0010871361040194164).abs() < 1e-12);
}
#[test]
fn a_current_coupon_forecasts_then_reads_todays_fixing() {
let (settings, curve, sofr) = common_vars(Date::new(23, November, 2021));
curve.link_to(flat_rate(settings.evaluation_date().unwrap(), 0.0010));
let coupon = make_coupon(
sofr.clone(),
Date::new(10, November, 2021),
Date::new(10, December, 2021),
);
let expected_rate = 0.000926701551;
assert!((coupon.rate().unwrap() - expected_rate).abs() < 1e-12);
let expected_amount = NOTIONAL * expected_rate * 30.0 / 360.0;
assert!((coupon.amount().unwrap() - expected_amount).abs() < 1e-8);
sofr.add_fixing(Date::new(23, November, 2021), 0.0007)
.unwrap();
let expected_rate = 0.000916700760;
assert!((coupon.rate().unwrap() - expected_rate).abs() < 1e-12);
let expected_amount = NOTIONAL * expected_rate * 30.0 / 360.0;
assert!((coupon.amount().unwrap() - expected_amount).abs() < 1e-8);
}
#[test]
fn a_future_coupon_forecasts_every_fixing() {
let (settings, curve, sofr) = common_vars(Date::new(23, November, 2021));
curve.link_to(flat_rate(settings.evaluation_date().unwrap(), 0.0010));
let coupon = make_coupon(
sofr,
Date::new(10, December, 2021),
Date::new(10, January, 2022),
);
let expected_rate = 0.001000043057;
assert!((coupon.rate().unwrap() - expected_rate).abs() < 1e-12);
let expected_amount = NOTIONAL * expected_rate * 31.0 / 360.0;
assert!((coupon.amount().unwrap() - expected_amount).abs() < 1e-8);
}
#[test]
fn a_coupon_prices_when_today_is_a_holiday() {
let (settings, curve, sofr) = common_vars(Date::new(23, November, 2021));
settings.set_evaluation_date(Date::new(20, November, 2021));
curve.link_to(flat_rate(Date::new(20, November, 2021), 0.0010));
let coupon = make_coupon(
sofr,
Date::new(10, November, 2021),
Date::new(10, December, 2021),
);
let expected_rate = 0.000930035180;
assert!((coupon.rate().unwrap() - expected_rate).abs() < 1e-12);
let expected_amount = NOTIONAL * expected_rate * 30.0 / 360.0;
assert!((coupon.amount().unwrap() - expected_amount).abs() < 1e-8);
}
#[test]
fn accrued_amount_in_the_past_compounds_up_to_the_date() {
let (_settings, _curve, sofr) = common_vars(Date::new(23, November, 2021));
let coupon = make_coupon(
sofr,
Date::new(18, October, 2021),
Date::new(18, January, 2022),
);
let expected_amount = NOTIONAL * 0.000987136104 * 31.0 / 360.0;
let accrued = coupon
.accrued_amount(Date::new(18, November, 2021))
.unwrap();
assert!((accrued - expected_amount).abs() < 1e-8);
}
}