use crate::currency::Currency;
use crate::errors::QlResult;
use crate::indexes::index::Index;
use crate::patterns::observable::{Observable, Observer, ResetThenNotify};
use crate::settings::Settings;
use crate::shared::{Shared, SharedMut};
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};
use crate::{fail, require};
pub struct InterestRateIndexBase {
family_name: String,
tenor: Period,
fixing_days: Natural,
currency: Currency,
day_counter: DayCounter,
fixing_calendar: Calendar,
name: String,
settings: Shared<Settings<Date>>,
observable: Shared<Observable>,
forwarder: SharedMut<ResetThenNotify>,
}
impl InterestRateIndexBase {
pub fn new(
family_name: String,
tenor: Period,
fixing_days: Natural,
currency: Currency,
fixing_calendar: Calendar,
day_counter: DayCounter,
settings: Shared<Settings<Date>>,
) -> Self {
let tenor = normalize_tenor(tenor);
let name = compose_name(&family_name, tenor, fixing_days, &day_counter);
let (observable, forwarder) = ResetThenNotify::forwarder();
let observer = forwarder.clone() as SharedMut<dyn Observer>;
settings.register_eval_date_observer(&observer);
settings.register_fixing_observer(&name, &observer);
InterestRateIndexBase {
family_name,
tenor,
fixing_days,
currency,
day_counter,
fixing_calendar,
name,
settings,
observable,
forwarder,
}
}
pub fn observable(&self) -> &Observable {
&self.observable
}
pub(crate) fn observer(&self) -> SharedMut<dyn Observer> {
self.forwarder.clone() as SharedMut<dyn Observer>
}
pub(crate) fn settings(&self) -> &Shared<Settings<Date>> {
&self.settings
}
}
fn normalize_tenor(tenor: Period) -> Period {
if tenor.units() == TimeUnit::Months && tenor.length() % 12 == 0 {
Period::new(tenor.length() / 12, TimeUnit::Years)
} else {
tenor
}
}
fn compose_name(
family_name: &str,
tenor: Period,
fixing_days: Natural,
day_counter: &DayCounter,
) -> String {
let period = if tenor == Period::new(1, TimeUnit::Days) {
match fixing_days {
0 => "ON".to_string(),
1 => "TN".to_string(),
2 => "SN".to_string(),
_ => format!("{tenor}"),
}
} else {
format!("{tenor}")
};
format!("{family_name}{period} {}", day_counter.name())
}
pub trait InterestRateIndex {
fn base(&self) -> &InterestRateIndexBase;
fn maturity_date(&self, value_date: Date) -> QlResult<Date>;
fn forecast_fixing(&self, fixing_date: Date) -> QlResult<Rate>;
fn family_name(&self) -> &str {
&self.base().family_name
}
fn tenor(&self) -> Period {
self.base().tenor
}
fn fixing_days(&self) -> Natural {
self.base().fixing_days
}
fn currency(&self) -> &Currency {
&self.base().currency
}
fn day_counter(&self) -> &DayCounter {
&self.base().day_counter
}
fn fixing_date(&self, value_date: Date) -> Date {
let base = self.base();
base.fixing_calendar.advance(
value_date,
-(base.fixing_days as Integer),
TimeUnit::Days,
BusinessDayConvention::Following,
false,
)
}
fn value_date(&self, fixing_date: Date) -> QlResult<Date> {
let base = self.base();
require!(
base.fixing_calendar.is_business_day(fixing_date),
"{fixing_date:?} is not a valid fixing date"
);
Ok(base.fixing_calendar.advance(
fixing_date,
base.fixing_days as Integer,
TimeUnit::Days,
BusinessDayConvention::Following,
false,
))
}
}
impl<T: InterestRateIndex> Index for T {
fn name(&self) -> String {
self.base().name.clone()
}
fn fixing_calendar(&self) -> Calendar {
self.base().fixing_calendar.clone()
}
fn is_valid_fixing_date(&self, fixing_date: Date) -> bool {
self.base().fixing_calendar.is_business_day(fixing_date)
}
fn settings(&self) -> &Settings<Date> {
&self.base().settings
}
fn observable(&self) -> &Observable {
&self.base().observable
}
fn fixing(&self, fixing_date: Date, forecast_todays_fixing: bool) -> QlResult<Rate> {
require!(
self.is_valid_fixing_date(fixing_date),
"Fixing date {fixing_date:?} is not valid"
);
let today = match self.settings().evaluation_date() {
Some(today) => today,
None => fail!("no evaluation date set: an index fixing needs a reference date"),
};
if fixing_date > today || (fixing_date == today && forecast_todays_fixing) {
return self.forecast_fixing(fixing_date);
}
if fixing_date < today || self.settings().enforces_todays_historic_fixings() {
return match self.settings().fixing(&self.name(), fixing_date) {
Some(rate) => Ok(rate),
None => fail!("Missing {} fixing for {fixing_date:?}", self.name()),
};
}
if let Some(rate) = self.settings().fixing(&self.name(), fixing_date) {
return Ok(rate);
}
self.forecast_fixing(fixing_date)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shared::{SharedMut, shared, shared_mut};
use crate::time::calendars::target::Target;
use crate::time::date::Month;
use crate::time::daycounters::actual360::Actual360;
fn settings_on(today: Date) -> Shared<Settings<Date>> {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(today);
settings
}
const FORECAST: Rate = 0.05;
struct TestRateIndex {
base: InterestRateIndexBase,
}
impl TestRateIndex {
fn euribor(fixing_days: Natural, settings: Shared<Settings<Date>>) -> Self {
TestRateIndex {
base: InterestRateIndexBase::new(
"Euribor".into(),
Period::new(6, TimeUnit::Months),
fixing_days,
Currency::eur(),
Target::new(),
Actual360::new(),
settings,
),
}
}
}
impl InterestRateIndex for TestRateIndex {
fn base(&self) -> &InterestRateIndexBase {
&self.base
}
fn maturity_date(&self, value_date: Date) -> QlResult<Date> {
Ok(self.base.fixing_calendar.advance_by_period(
value_date,
self.base.tenor,
BusinessDayConvention::Following,
false,
))
}
fn forecast_fixing(&self, _fixing_date: Date) -> QlResult<Rate> {
Ok(FORECAST)
}
}
#[test]
fn tenor_normalizes_and_name_is_composed() {
let settings = shared(Settings::<Date>::new());
let base = InterestRateIndexBase::new(
"Euribor".into(),
Period::new(12, TimeUnit::Months),
2,
Currency::eur(),
Target::new(),
Actual360::new(),
settings,
);
assert_eq!(base.tenor, Period::new(1, TimeUnit::Years));
assert_eq!(base.name, "Euribor1Y Actual/360");
}
#[test]
fn one_day_tenor_names_on_tn_sn_by_fixing_days() {
let day = Period::new(1, TimeUnit::Days);
let dc = Actual360::new();
assert_eq!(compose_name("Eonia", day, 0, &dc), "EoniaON Actual/360");
assert_eq!(compose_name("Eonia", day, 1, &dc), "EoniaTN Actual/360");
assert_eq!(compose_name("Eonia", day, 2, &dc), "EoniaSN Actual/360");
assert_eq!(compose_name("Eonia", day, 3, &dc), "Eonia1D Actual/360");
}
#[test]
fn fixing_and_value_date_round_trip_across_a_weekend() {
let settings = shared(Settings::<Date>::new());
let index = TestRateIndex::euribor(2, settings);
let value_date = Date::new(15, Month::June, 2026);
let fixing_date = index.fixing_date(value_date);
assert_eq!(fixing_date, Date::new(11, Month::June, 2026));
assert_eq!(index.value_date(fixing_date).unwrap(), value_date);
}
#[test]
fn value_date_rejects_an_invalid_fixing_date() {
let settings = shared(Settings::<Date>::new());
let index = TestRateIndex::euribor(2, settings);
assert!(index.value_date(Date::new(13, Month::June, 2026)).is_err());
}
#[test]
fn fixing_on_an_invalid_date_is_an_error() {
let today = Date::new(15, Month::June, 2026);
let index = TestRateIndex::euribor(2, settings_on(today));
assert!(
index
.fixing(Date::new(13, Month::June, 2026), false)
.is_err()
);
}
#[test]
fn fixing_without_an_evaluation_date_is_an_error() {
let settings = shared(Settings::<Date>::new());
let index = TestRateIndex::euribor(2, settings);
assert!(
index
.fixing(Date::new(15, Month::June, 2026), false)
.is_err()
);
}
#[test]
fn future_date_forecasts() {
let today = Date::new(15, Month::June, 2026);
let index = TestRateIndex::euribor(2, settings_on(today));
let tomorrow = Date::new(16, Month::June, 2026);
assert_eq!(index.fixing(tomorrow, false).unwrap(), FORECAST);
}
#[test]
fn today_forecasts_when_asked_even_with_a_stored_fixing() {
let today = Date::new(15, Month::June, 2026);
let settings = settings_on(today);
let index = TestRateIndex::euribor(2, settings.clone());
settings.add_fixing(&index.base.name, today, 0.01).unwrap();
assert_eq!(index.fixing(today, true).unwrap(), FORECAST);
}
#[test]
fn today_reads_a_stored_fixing_when_not_forecasting() {
let today = Date::new(15, Month::June, 2026);
let settings = settings_on(today);
let index = TestRateIndex::euribor(2, settings.clone());
settings.add_fixing(&index.base.name, today, 0.01).unwrap();
assert_eq!(index.fixing(today, false).unwrap(), 0.01);
}
#[test]
fn today_forecasts_when_no_stored_fixing_and_not_enforced() {
let today = Date::new(15, Month::June, 2026);
let index = TestRateIndex::euribor(2, settings_on(today));
assert_eq!(index.fixing(today, false).unwrap(), FORECAST);
}
#[test]
fn today_missing_fixing_is_an_error_when_enforced() {
let today = Date::new(15, Month::June, 2026);
let settings = settings_on(today);
settings.set_enforces_todays_historic_fixings(true);
let index = TestRateIndex::euribor(2, settings);
assert!(index.fixing(today, false).is_err());
}
#[test]
fn today_reads_a_stored_fixing_when_enforced() {
let today = Date::new(15, Month::June, 2026);
let settings = settings_on(today);
settings.set_enforces_todays_historic_fixings(true);
let index = TestRateIndex::euribor(2, settings.clone());
settings.add_fixing(&index.base.name, today, 0.01).unwrap();
assert_eq!(index.fixing(today, false).unwrap(), 0.01);
}
#[test]
fn past_stored_fixing_is_returned() {
let today = Date::new(15, Month::June, 2026);
let settings = settings_on(today);
let index = TestRateIndex::euribor(2, settings.clone());
let past = Date::new(12, Month::June, 2026);
settings.add_fixing(&index.base.name, past, 0.01).unwrap();
assert_eq!(index.fixing(past, false).unwrap(), 0.01);
}
#[test]
fn past_missing_fixing_is_an_error() {
let today = Date::new(15, Month::June, 2026);
let index = TestRateIndex::euribor(2, settings_on(today));
let past = Date::new(12, Month::June, 2026);
assert!(index.fixing(past, false).is_err());
}
struct Flag {
up: bool,
}
impl Observer for Flag {
fn update(&mut self) {
self.up = true;
}
}
#[test]
fn adding_a_fixing_notifies_the_index() {
let today = Date::new(15, Month::June, 2026);
let settings = settings_on(today);
let index = TestRateIndex::euribor(2, settings.clone());
let flag = shared_mut(Flag { up: false });
index
.base
.observable()
.register_observer(&(flag.clone() as SharedMut<dyn Observer>));
settings
.add_fixing(&index.base.name, Date::new(12, Month::June, 2026), 0.01)
.unwrap();
assert!(flag.borrow().up);
}
#[test]
fn changing_the_evaluation_date_notifies_the_index() {
let today = Date::new(15, Month::June, 2026);
let settings = settings_on(today);
let index = TestRateIndex::euribor(2, settings.clone());
let flag = shared_mut(Flag { up: false });
index
.base
.observable()
.register_observer(&(flag.clone() as SharedMut<dyn Observer>));
settings.set_evaluation_date(Date::new(16, Month::June, 2026));
assert!(flag.borrow().up);
}
}