use crate::errors::QlResult;
use crate::handle::Handle;
use crate::indexes::index::Index;
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::time::timeunit::TimeUnit;
use crate::types::{Natural, Rate, Time};
use crate::{currency::Currency, require};
pub struct IborIndex {
base: InterestRateIndexBase,
convention: BusinessDayConvention,
end_of_month: bool,
term_structure: Handle<dyn YieldTermStructure>,
}
impl IborIndex {
#[allow(clippy::too_many_arguments)]
pub fn new(
family_name: String,
tenor: Period,
settlement_days: Natural,
currency: Currency,
fixing_calendar: Calendar,
convention: BusinessDayConvention,
end_of_month: bool,
day_counter: DayCounter,
forwarding: Handle<dyn YieldTermStructure>,
settings: Shared<Settings<Date>>,
) -> IborIndex {
let base = InterestRateIndexBase::new(
family_name,
tenor,
settlement_days,
currency,
fixing_calendar,
day_counter,
settings,
);
forwarding.register_observer(&base.observer());
IborIndex {
base,
convention,
end_of_month,
term_structure: forwarding,
}
}
pub fn business_day_convention(&self) -> BusinessDayConvention {
self.convention
}
pub fn end_of_month(&self) -> bool {
self.end_of_month
}
pub fn forwarding_term_structure(&self) -> &Handle<dyn YieldTermStructure> {
&self.term_structure
}
pub fn clone_with(&self, forwarding: Handle<dyn YieldTermStructure>) -> IborIndex {
IborIndex::new(
self.family_name().to_string(),
self.tenor(),
self.fixing_days(),
self.currency().clone(),
self.fixing_calendar(),
self.convention,
self.end_of_month,
self.day_counter().clone(),
forwarding,
self.base.settings().clone(),
)
}
pub(crate) fn forecast_fixing_between(&self, d1: Date, d2: Date, t: Time) -> QlResult<Rate> {
require!(
!self.term_structure.is_empty(),
"null term structure set to this instance of {}",
self.name()
);
let curve = self.term_structure.current_link()?;
let disc1 = curve.discount_date(d1, false)?;
let disc2 = curve.discount_date(d2, false)?;
Ok((disc1 / disc2 - 1.0) / t)
}
}
impl InterestRateIndex for IborIndex {
fn base(&self) -> &InterestRateIndexBase {
&self.base
}
fn maturity_date(&self, value_date: Date) -> QlResult<Date> {
Ok(self.fixing_calendar().advance_by_period(
value_date,
self.tenor(),
self.convention,
self.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.forecast_fixing_between(d1, d2, t)
}
}
pub struct OvernightIndex(Shared<IborIndex>);
impl OvernightIndex {
pub fn new(
family_name: String,
settlement_days: Natural,
currency: Currency,
fixing_calendar: Calendar,
day_counter: DayCounter,
forwarding: Handle<dyn YieldTermStructure>,
settings: Shared<Settings<Date>>,
) -> OvernightIndex {
OvernightIndex(shared(IborIndex::new(
family_name,
Period::new(1, TimeUnit::Days),
settlement_days,
currency,
fixing_calendar,
BusinessDayConvention::Following,
false,
day_counter,
forwarding,
settings,
)))
}
pub fn ibor_index(&self) -> Shared<IborIndex> {
self.0.clone()
}
pub fn clone_with(&self, forwarding: Handle<dyn YieldTermStructure>) -> Shared<OvernightIndex> {
shared(OvernightIndex(shared(self.0.clone_with(forwarding))))
}
pub fn business_day_convention(&self) -> BusinessDayConvention {
self.0.business_day_convention()
}
pub fn end_of_month(&self) -> bool {
self.0.end_of_month()
}
pub fn forwarding_term_structure(&self) -> &Handle<dyn YieldTermStructure> {
self.0.forwarding_term_structure()
}
}
impl InterestRateIndex for OvernightIndex {
fn base(&self) -> &InterestRateIndexBase {
self.0.base()
}
fn maturity_date(&self, value_date: Date) -> QlResult<Date> {
self.0.maturity_date(value_date)
}
fn forecast_fixing(&self, fixing_date: Date) -> QlResult<Rate> {
self.0.forecast_fixing(fixing_date)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::handle::RelinkableHandle;
use crate::interestrate::Compounding;
use crate::patterns::observable::Observer;
use crate::shared::{SharedMut, shared, shared_mut};
use crate::termstructures::yields::FlatForward;
use crate::time::calendars::target::Target;
use crate::time::calendars::unitedstates::{Market, UnitedStates};
use crate::time::date::Month;
use crate::time::daycounters::actual360::Actual360;
use crate::time::frequency::Frequency;
use crate::time::timeunit::TimeUnit;
fn settings_on(today: Date) -> Shared<Settings<Date>> {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(today);
settings
}
fn ibor(
tenor: Period,
forwarding: Handle<dyn YieldTermStructure>,
settings: Shared<Settings<Date>>,
) -> IborIndex {
IborIndex::new(
"foo".into(),
tenor,
2,
Currency::eur(),
Target::new(),
BusinessDayConvention::Following,
false,
Actual360::new(),
forwarding,
settings,
)
}
fn flat_curve(reference: Date, rate: Rate) -> Handle<dyn YieldTermStructure> {
Handle::new(shared(FlatForward::with_rate(
reference,
rate,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>)
}
#[test]
fn twelve_months_and_one_year_yield_the_same_name() {
let settings = shared(Settings::<Date>::new());
let i12m = ibor(
Period::new(12, TimeUnit::Months),
Handle::empty(),
settings.clone(),
);
let i1y = ibor(Period::new(1, TimeUnit::Years), Handle::empty(), settings);
assert_eq!(i12m.name(), i1y.name());
}
#[test]
fn six_day_index_matures_before_seven_day_index() {
let settings = shared(Settings::<Date>::new());
let i6d = ibor(
Period::new(6, TimeUnit::Days),
Handle::empty(),
settings.clone(),
);
let i7d = ibor(Period::new(7, TimeUnit::Days), Handle::empty(), settings);
let test_date = Date::new(28, Month::April, 2023);
assert!(i6d.maturity_date(test_date).unwrap() < i7d.maturity_date(test_date).unwrap());
}
#[test]
fn historical_fixing_is_shared_by_name_and_cleared() {
let today = Date::new(15, Month::June, 2026);
let settings = settings_on(today);
let ibor6m = ibor(
Period::new(6, TimeUnit::Months),
Handle::empty(),
settings.clone(),
);
let ibor3m = ibor(
Period::new(3, TimeUnit::Months),
Handle::empty(),
settings.clone(),
);
let ibor6m_a = ibor(Period::new(6, TimeUnit::Months), Handle::empty(), settings);
let mut day = today;
while !ibor6m.is_valid_fixing_date(day) {
day -= 1;
}
ibor6m.add_fixing(day, 0.01).unwrap();
assert!(!ibor3m.has_historical_fixing(day));
assert!(ibor6m.has_historical_fixing(day));
assert!(ibor6m_a.has_historical_fixing(day));
ibor6m.clear_fixings();
assert!(!ibor6m.has_historical_fixing(day));
assert!(!ibor6m_a.has_historical_fixing(day));
}
#[test]
fn forecast_fixing_reads_the_simple_forward_off_the_curve() {
let today = Date::new(15, Month::June, 2026);
let settings = settings_on(today);
let rate = 0.05;
let curve = flat_curve(today, rate);
let index = ibor(Period::new(6, TimeUnit::Months), curve.clone(), settings);
let fixing_date = Date::new(15, Month::July, 2026);
let d1 = index.value_date(fixing_date).unwrap();
let d2 = index.maturity_date(d1).unwrap();
let t = Actual360::new().year_fraction(d1, d2);
let forecast = index.forecast_fixing(fixing_date).unwrap();
let link = curve.current_link().unwrap();
let disc1 = link.discount_date(d1, false).unwrap();
let disc2 = link.discount_date(d2, false).unwrap();
assert!((forecast - (disc1 / disc2 - 1.0) / t).abs() < 1e-12);
let analytic = ((rate * t).exp() - 1.0) / t;
assert!((forecast - analytic).abs() < 1e-12);
assert!(forecast > 0.0);
}
#[test]
fn forecast_on_an_empty_curve_is_an_error() {
let today = Date::new(15, Month::June, 2026);
let settings = settings_on(today);
let index = ibor(Period::new(6, TimeUnit::Months), Handle::empty(), settings);
let err = index
.forecast_fixing(Date::new(15, Month::July, 2026))
.unwrap_err();
assert!(
err.to_string()
.contains("null term structure set to this instance of")
);
}
struct Flag {
up: bool,
}
impl Observer for Flag {
fn update(&mut self) {
self.up = true;
}
}
#[test]
fn relinking_the_forwarding_curve_notifies_the_index() {
let today = Date::new(15, Month::June, 2026);
let settings = settings_on(today);
let handle: RelinkableHandle<dyn YieldTermStructure> = RelinkableHandle::empty();
let index = ibor(Period::new(6, TimeUnit::Months), handle.handle(), settings);
let flag = shared_mut(Flag { up: false });
index
.base()
.observable()
.register_observer(&(flag.clone() as SharedMut<dyn Observer>));
handle.link_to(shared(FlatForward::with_rate(
today,
0.05,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>);
assert!(flag.borrow().up);
}
#[test]
fn clone_with_recurves_and_shares_fixing_history() {
let today = Date::new(15, Month::June, 2026);
let settings = settings_on(today);
let original = ibor(
Period::new(6, TimeUnit::Months),
flat_curve(today, 0.05),
settings,
);
let cloned_handle: RelinkableHandle<dyn YieldTermStructure> = RelinkableHandle::empty();
let clone = original.clone_with(cloned_handle.handle());
assert_eq!(clone.name(), original.name());
assert_eq!(clone.tenor(), original.tenor());
assert_eq!(clone.fixing_days(), original.fixing_days());
assert_eq!(
clone.business_day_convention(),
original.business_day_convention()
);
assert_eq!(clone.end_of_month(), original.end_of_month());
let fixing_date = Date::new(15, Month::July, 2026);
assert!(
clone.forecast_fixing(fixing_date).is_err(),
"clone starts on its own empty handle"
);
cloned_handle.link_to(shared(FlatForward::with_rate(
today,
0.02,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>);
let d1 = clone.value_date(fixing_date).unwrap();
let d2 = clone.maturity_date(d1).unwrap();
let t = Actual360::new().year_fraction(d1, d2);
let clone_forecast = clone.forecast_fixing(fixing_date).unwrap();
assert!((clone_forecast - ((0.02 * t).exp() - 1.0) / t).abs() < 1e-12);
let original_forecast = original.forecast_fixing(fixing_date).unwrap();
assert!(
(original_forecast - ((0.05 * t).exp() - 1.0) / t).abs() < 1e-12,
"the original still forecasts off its own curve"
);
let mut day = today;
while !original.is_valid_fixing_date(day) {
day -= 1;
}
original.add_fixing(day, 0.01).unwrap();
assert!(
clone.has_historical_fixing(day),
"a clone shares the original's fixing history by name"
);
}
#[test]
fn overnight_index_carries_the_overnight_configuration() {
let settings = shared(Settings::<Date>::new());
let index = OvernightIndex::new(
"SOFR".into(),
0,
Currency::usd(),
UnitedStates::new(Market::Sofr),
Actual360::new(),
Handle::empty(),
settings,
);
assert_eq!(index.tenor(), Period::new(1, TimeUnit::Days));
assert_eq!(index.fixing_days(), 0);
assert_eq!(
index.business_day_convention(),
BusinessDayConvention::Following
);
assert!(!index.end_of_month());
assert_eq!(index.name(), "SOFRON Actual/360");
assert_eq!(index.fixing_calendar().name(), "SOFR fixing calendar");
}
#[test]
fn overnight_index_forecasts_through_the_ibor_machinery() {
let today = Date::new(15, Month::June, 2026);
let settings = settings_on(today);
let rate = 0.05;
let index = OvernightIndex::new(
"ESTR".into(),
0,
Currency::eur(),
Target::new(),
Actual360::new(),
flat_curve(today, rate),
settings,
);
let fixing_date = Date::new(15, Month::July, 2026);
let d1 = index.value_date(fixing_date).unwrap();
let d2 = index.maturity_date(d1).unwrap();
let t = Actual360::new().year_fraction(d1, d2);
let forecast = index.forecast_fixing(fixing_date).unwrap();
let analytic = ((rate * t).exp() - 1.0) / t;
assert!((forecast - analytic).abs() < 1e-12);
}
}