use crate::currency::Currency;
use crate::errors::QlResult;
use crate::handle::Handle;
use crate::indexes::iborindex::IborIndex;
use crate::indexes::interestrateindex::{InterestRateIndex, InterestRateIndexBase};
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::types::{Natural, Rate};
pub struct CustomIborIndex {
ibor: Shared<IborIndex>,
}
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,
)
.with_separate_calendars(value_calendar, maturity_calendar),
),
}
}
pub fn upcast(&self) -> Shared<IborIndex> {
self.ibor.clone()
}
pub fn value_calendar(&self) -> Calendar {
self.ibor.value_calendar()
}
pub fn maturity_calendar(&self) -> Calendar {
self.ibor.maturity_calendar()
}
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)),
}
}
}
impl InterestRateIndex for CustomIborIndex {
fn base(&self) -> &InterestRateIndexBase {
self.ibor.base()
}
fn fixing_date(&self, value_date: Date) -> Date {
self.ibor.fixing_date(value_date)
}
fn value_date(&self, fixing_date: Date) -> QlResult<Date> {
self.ibor.value_date(fixing_date)
}
fn maturity_date(&self, value_date: Date) -> QlResult<Date> {
self.ibor.maturity_date(value_date)
}
fn forecast_fixing(&self, fixing_date: Date) -> QlResult<Rate> {
self.ibor.forecast_fixing(fixing_date)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::indexes::index::Index;
use crate::time::calendars::bespokecalendar::BespokeCalendar;
use crate::time::date::Month;
use crate::time::daycounters::actual360::Actual360;
use crate::time::timeunit::TimeUnit;
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 the_upcast_ibor_index_reproduces_the_three_calendar_dates() {
let (index, _, _, _) = custom_ibor();
let ibor = index.upcast();
assert_eq!(
ibor.value_date(Date::new(20, Month::January, 2025))
.unwrap(),
Date::new(23, Month::January, 2025)
);
assert_eq!(
ibor.fixing_date(Date::new(23, Month::January, 2025)),
Date::new(20, Month::January, 2025)
);
assert_eq!(
ibor.maturity_date(Date::new(23, Month::January, 2025))
.unwrap(),
Date::new(24, Month::April, 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);
}
}