use crate::currency::Currency;
use crate::errors::QlResult;
use crate::handle::Handle;
use crate::indexes::iborindex::IborIndex;
use crate::indexes::index::Index;
use crate::indexes::interestrateindex::{InterestRateIndex, InterestRateIndexBase};
use crate::require;
use crate::settings::Settings;
use crate::shared::{Shared, shared};
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendar::Calendar;
use crate::time::date::Date;
use crate::time::daycounter::DayCounter;
use crate::time::period::Period;
use crate::time::timeunit::TimeUnit;
use crate::types::{Integer, Natural, Rate};
pub struct CustomIborIndex {
ibor: Shared<IborIndex>,
value_calendar: Calendar,
maturity_calendar: Calendar,
}
impl CustomIborIndex {
#[allow(clippy::too_many_arguments)]
pub fn new(
family_name: String,
tenor: Period,
settlement_days: Natural,
currency: Currency,
fixing_calendar: Calendar,
value_calendar: Calendar,
maturity_calendar: Calendar,
convention: BusinessDayConvention,
end_of_month: bool,
day_counter: DayCounter,
forwarding: Handle<dyn YieldTermStructure>,
settings: Shared<Settings<Date>>,
) -> CustomIborIndex {
CustomIborIndex {
ibor: shared(IborIndex::new(
family_name,
tenor,
settlement_days,
currency,
fixing_calendar,
convention,
end_of_month,
day_counter,
forwarding,
settings,
)),
value_calendar,
maturity_calendar,
}
}
pub fn value_calendar(&self) -> Calendar {
self.value_calendar.clone()
}
pub fn maturity_calendar(&self) -> Calendar {
self.maturity_calendar.clone()
}
pub fn business_day_convention(&self) -> BusinessDayConvention {
self.ibor.business_day_convention()
}
pub fn end_of_month(&self) -> bool {
self.ibor.end_of_month()
}
pub fn clone_with(&self, forwarding: Handle<dyn YieldTermStructure>) -> CustomIborIndex {
CustomIborIndex {
ibor: shared(self.ibor.clone_with(forwarding)),
value_calendar: self.value_calendar.clone(),
maturity_calendar: self.maturity_calendar.clone(),
}
}
}
impl InterestRateIndex for CustomIborIndex {
fn base(&self) -> &InterestRateIndexBase {
self.ibor.base()
}
fn fixing_date(&self, value_date: Date) -> Date {
let fixing_date = self.value_calendar.advance(
value_date,
-(self.fixing_days() as Integer),
TimeUnit::Days,
BusinessDayConvention::Following,
false,
);
self.fixing_calendar()
.adjust(fixing_date, BusinessDayConvention::Preceding)
}
fn value_date(&self, fixing_date: Date) -> QlResult<Date> {
require!(
self.is_valid_fixing_date(fixing_date),
"{fixing_date:?} is not a valid fixing date"
);
let d = self.value_calendar.advance(
fixing_date,
self.fixing_days() as Integer,
TimeUnit::Days,
BusinessDayConvention::Following,
false,
);
Ok(self
.maturity_calendar
.adjust(d, BusinessDayConvention::Following))
}
fn maturity_date(&self, value_date: Date) -> QlResult<Date> {
Ok(self.maturity_calendar.advance_by_period(
value_date,
self.tenor(),
self.ibor.business_day_convention(),
self.ibor.end_of_month(),
))
}
fn forecast_fixing(&self, fixing_date: Date) -> QlResult<Rate> {
let d1 = self.value_date(fixing_date)?;
let d2 = self.maturity_date(d1)?;
let t = self.day_counter().year_fraction(d1, d2);
let positive_time = t > 0.0;
require!(
positive_time,
"cannot calculate forward rate between {d1:?} and {d2:?}: non positive time ({t}) using {} daycounter",
self.day_counter().name()
);
self.ibor.forecast_fixing_between(d1, d2, t)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::time::calendars::bespokecalendar::BespokeCalendar;
use crate::time::date::Month;
use crate::time::daycounters::actual360::Actual360;
fn custom_ibor() -> (CustomIborIndex, Calendar, Calendar, Calendar) {
let fix_cal = BespokeCalendar::new("Fixings").calendar();
fix_cal.add_holiday(Date::new(8, Month::January, 2025));
let val_cal = BespokeCalendar::new("Value").calendar();
val_cal.add_holiday(Date::new(21, Month::January, 2025));
let mat_cal = BespokeCalendar::new("Maturity").calendar();
mat_cal.add_holiday(Date::new(7, Month::January, 2025));
mat_cal.add_holiday(Date::new(15, Month::January, 2025));
mat_cal.add_holiday(Date::new(23, Month::April, 2025));
mat_cal.add_holiday(Date::new(30, Month::April, 2025));
let settings = shared(Settings::<Date>::new());
let index = CustomIborIndex::new(
"Custom Ibor".into(),
Period::new(3, TimeUnit::Months),
2,
Currency::new("", "", 0, "", "", 0),
fix_cal.clone(),
val_cal.clone(),
mat_cal.clone(),
BusinessDayConvention::ModifiedFollowing,
true,
Actual360::new(),
Handle::empty(),
settings,
);
(index, fix_cal, val_cal, mat_cal)
}
#[test]
fn the_three_calendars_survive_construction_and_clone() {
let (index, fix_cal, val_cal, mat_cal) = custom_ibor();
let clone = index.clone_with(Handle::empty());
for index in [&index, &clone] {
assert_eq!(index.fixing_calendar(), fix_cal);
assert_eq!(index.value_calendar(), val_cal);
assert_eq!(index.maturity_calendar(), mat_cal);
}
}
#[test]
fn value_date_rejects_a_fixing_calendar_holiday() {
let (index, _, _, _) = custom_ibor();
let clone = index.clone_with(Handle::empty());
let holiday = Date::new(8, Month::January, 2025);
for index in [&index, &clone] {
assert!(!index.is_valid_fixing_date(holiday));
assert!(index.value_date(holiday).is_err());
}
}
#[test]
fn value_date_advances_on_value_and_adjusts_on_maturity() {
let (index, _, _, _) = custom_ibor();
let clone = index.clone_with(Handle::empty());
for index in [&index, &clone] {
assert_eq!(
index
.value_date(Date::new(7, Month::January, 2025))
.unwrap(),
Date::new(9, Month::January, 2025)
);
assert_eq!(
index
.value_date(Date::new(13, Month::January, 2025))
.unwrap(),
Date::new(16, Month::January, 2025)
);
assert_eq!(
index
.value_date(Date::new(20, Month::January, 2025))
.unwrap(),
Date::new(23, Month::January, 2025)
);
}
}
#[test]
fn fixing_date_goes_back_on_value_and_adjusts_preceding_on_fixing() {
let (index, _, _, _) = custom_ibor();
let clone = index.clone_with(Handle::empty());
for index in [&index, &clone] {
assert_eq!(
index.fixing_date(Date::new(23, Month::January, 2025)),
Date::new(20, Month::January, 2025)
);
assert_eq!(
index.fixing_date(Date::new(16, Month::January, 2025)),
Date::new(14, Month::January, 2025)
);
assert_eq!(
index.fixing_date(Date::new(10, Month::January, 2025)),
Date::new(7, Month::January, 2025)
);
}
}
#[test]
fn maturity_date_advances_on_the_maturity_calendar() {
let (index, _, _, _) = custom_ibor();
let clone = index.clone_with(Handle::empty());
for index in [&index, &clone] {
assert_eq!(
index
.maturity_date(Date::new(23, Month::January, 2025))
.unwrap(),
Date::new(24, Month::April, 2025)
);
assert_eq!(
index
.maturity_date(Date::new(30, Month::January, 2025))
.unwrap(),
Date::new(29, Month::April, 2025)
);
assert_eq!(
index
.maturity_date(Date::new(28, Month::February, 2025))
.unwrap(),
Date::new(31, Month::May, 2025)
);
}
}
#[test]
fn forecast_fixing_reads_the_curve_between_the_overridden_dates() {
use crate::math::interpolations::linear::Linear;
use crate::termstructures::yields::InterpolatedZeroCurve;
let curve = shared(
InterpolatedZeroCurve::new(
vec![
Date::new(2, Month::January, 2025),
Date::new(1, Month::July, 2025),
],
vec![0.02, 0.10],
Actual360::new(),
Linear,
)
.expect("two well-ordered nodes"),
) as Shared<dyn YieldTermStructure>;
let (index, _, _, _) = custom_ibor();
let index = index.clone_with(Handle::new(Shared::clone(&curve)));
let fixing = Date::new(20, Month::January, 2025);
let d1 = index.value_date(fixing).unwrap();
let d2 = index.maturity_date(d1).unwrap();
assert_eq!(d1, Date::new(23, Month::January, 2025));
assert_eq!(d2, Date::new(24, Month::April, 2025));
let simple_forward = |d1: Date, d2: Date| {
let disc1 = curve.discount_date(d1, false).unwrap();
let disc2 = curve.discount_date(d2, false).unwrap();
(disc1 / disc2 - 1.0) / Actual360::new().year_fraction(d1, d2)
};
let expected = simple_forward(d1, d2);
let single_calendar = simple_forward(
Date::new(22, Month::January, 2025),
Date::new(22, Month::April, 2025),
);
assert!(
(expected - single_calendar).abs() > 1.0e-4,
"the fixture no longer discriminates the date pairs"
);
assert!((index.forecast_fixing(fixing).unwrap() - expected).abs() < 1.0e-15);
}
}