use crate::currency::Currency;
use crate::errors::QlResult;
use crate::handle::Handle;
use crate::indexes::index::Index;
use crate::indexes::region::Region;
use crate::patterns::observable::{Observable, Observer, ResetThenNotify};
use crate::settings::Settings;
use crate::shared::{Shared, SharedMut};
use crate::termstructures::inflation::inflationtermstructure::ZeroInflationTermStructure;
use crate::time::calendar::Calendar;
use crate::time::calendars::nullcalendar::NullCalendar;
use crate::time::date::{Date, Month};
use crate::time::daycounter::DayCounter;
use crate::time::frequency::Frequency;
use crate::time::period::Period;
use crate::time::timeunit::TimeUnit;
use crate::types::{Integer, Rate, Time};
pub fn inflation_period(date: Date, frequency: Frequency) -> QlResult<(Date, Date)> {
let month = date.month().ordinal();
let year = date.year();
let (start_month, end_month) = match frequency {
Frequency::Annual
| Frequency::Semiannual
| Frequency::EveryFourthMonth
| Frequency::Quarterly
| Frequency::Bimonthly => {
let n_months = 12 / (frequency as Integer);
let start = month - (month - 1) % n_months;
(start, start + n_months - 1)
}
Frequency::Monthly => (month, month),
_ => crate::fail!("frequency not handled: {frequency}"),
};
Ok((
Date::new(1, Month::from_ordinal(start_month), year),
Date::end_of_month(Date::new(1, Month::from_ordinal(end_month), year)),
))
}
pub fn inflation_year_fraction(
frequency: Frequency,
index_is_interpolated: bool,
day_counter: &DayCounter,
d1: Date,
d2: Date,
) -> QlResult<Time> {
if index_is_interpolated {
return Ok(day_counter.year_fraction(d1, d2));
}
let (first_of_d1, _) = inflation_period(d1, frequency)?;
let (first_of_d2, _) = inflation_period(d2, frequency)?;
Ok(day_counter.year_fraction(first_of_d1, first_of_d2))
}
pub struct InflationIndexBase {
family_name: String,
region: Region,
revised: bool,
frequency: Frequency,
availability_lag: Period,
currency: Currency,
name: String,
settings: Shared<Settings<Date>>,
observable: Shared<Observable>,
forwarder: SharedMut<ResetThenNotify>,
}
impl InflationIndexBase {
pub fn new(
family_name: String,
region: Region,
revised: bool,
frequency: Frequency,
availability_lag: Period,
currency: Currency,
settings: Shared<Settings<Date>>,
) -> Self {
let name = format!("{} {}", region.name(), family_name);
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);
InflationIndexBase {
family_name,
region,
revised,
frequency,
availability_lag,
currency,
name,
settings,
observable,
forwarder,
}
}
pub fn name(&self) -> String {
self.name.clone()
}
pub fn fixing_calendar(&self) -> Calendar {
NullCalendar::new()
}
pub fn observable(&self) -> &Observable {
&self.observable
}
pub fn observer(&self) -> SharedMut<dyn Observer> {
self.forwarder.clone() as SharedMut<dyn Observer>
}
pub fn settings(&self) -> &Shared<Settings<Date>> {
&self.settings
}
pub fn add_fixing(&self, fixing_date: Date, value: Rate) -> QlResult<()> {
let (first, last) = inflation_period(fixing_date, self.frequency)?;
let days = last - first + 1;
let fixings = (0..days).map(|i| (first + i, value));
self.settings.add_fixings(&self.name, fixings)
}
}
pub trait InflationIndex: Index {
fn inflation_base(&self) -> &InflationIndexBase;
fn family_name(&self) -> &str {
&self.inflation_base().family_name
}
fn region(&self) -> &Region {
&self.inflation_base().region
}
fn revised(&self) -> bool {
self.inflation_base().revised
}
fn frequency(&self) -> Frequency {
self.inflation_base().frequency
}
fn availability_lag(&self) -> Period {
self.inflation_base().availability_lag
}
fn currency(&self) -> &Currency {
&self.inflation_base().currency
}
}
pub struct ZeroInflationIndex {
base: InflationIndexBase,
term_structure: Handle<dyn ZeroInflationTermStructure>,
}
impl ZeroInflationIndex {
pub fn new(
family_name: String,
region: Region,
revised: bool,
frequency: Frequency,
availability_lag: Period,
currency: Currency,
settings: Shared<Settings<Date>>,
) -> Self {
ZeroInflationIndex {
base: InflationIndexBase::new(
family_name,
region,
revised,
frequency,
availability_lag,
currency,
settings,
),
term_structure: Handle::empty(),
}
}
pub fn with_term_structure(
self,
term_structure: Handle<dyn ZeroInflationTermStructure>,
) -> ZeroInflationIndex {
term_structure.register_observer(&self.base.observer());
ZeroInflationIndex {
base: self.base,
term_structure,
}
}
pub fn clone_linked_to(
&self,
term_structure: Handle<dyn ZeroInflationTermStructure>,
) -> ZeroInflationIndex {
ZeroInflationIndex::new(
self.base.family_name.clone(),
self.base.region.clone(),
self.base.revised,
self.base.frequency,
self.base.availability_lag,
self.base.currency.clone(),
Shared::clone(self.base.settings()),
)
.with_term_structure(term_structure)
}
pub fn term_structure(&self) -> &Handle<dyn ZeroInflationTermStructure> {
&self.term_structure
}
pub fn last_fixing_date(&self) -> QlResult<Date> {
let last = match self.base.settings().last_fixing_date(&self.base.name()) {
Some(date) => date,
None => crate::fail!("no fixings stored for {}", self.base.name()),
};
Ok(inflation_period(last, self.frequency())?.0)
}
pub fn needs_forecast(&self, fixing_date: Date) -> QlResult<bool> {
let today = match self.base.settings().evaluation_date() {
Some(today) => today,
None => crate::fail!("no evaluation date set: an index fixing needs a reference date"),
};
let frequency = self.frequency();
let latest_possible = inflation_period(today - self.availability_lag(), frequency)?;
let latest_needed_date = inflation_period(fixing_date, frequency)?.0;
if latest_needed_date < latest_possible.0 {
Ok(false)
} else if latest_needed_date > latest_possible.1 {
Ok(true)
} else {
Ok(self
.base
.settings()
.fixing(&self.base.name(), latest_needed_date)
.is_none())
}
}
fn forecast_fixing(&self, fixing_date: Date) -> QlResult<Rate> {
let curve = self.term_structure.current_link()?;
let base_date = curve.base_date();
crate::require!(
!self.needs_forecast(base_date)?,
"{} index fixing at base date {base_date} is not available",
self.base.name()
);
let base_fixing = self.fixing(base_date, false)?;
let (first_date_in_period, _) = inflation_period(fixing_date, self.frequency())?;
let z1 = curve.zero_rate_date(first_date_in_period, false)?;
let t1 = inflation_year_fraction(
self.frequency(),
false,
&curve.require_day_counter()?,
base_date,
first_date_in_period,
)?;
if z1 <= -1.0 {
return Ok(0.0);
}
Ok(base_fixing * (1.0 + z1).powf(t1))
}
}
impl Index for ZeroInflationIndex {
fn name(&self) -> String {
self.base.name()
}
fn fixing_calendar(&self) -> Calendar {
self.base.fixing_calendar()
}
fn is_valid_fixing_date(&self, _fixing_date: Date) -> bool {
true
}
fn fixing(&self, fixing_date: Date, _forecast_todays_fixing: bool) -> QlResult<Rate> {
if self.needs_forecast(fixing_date)? {
return self.forecast_fixing(fixing_date);
}
let (first, _) = inflation_period(fixing_date, self.frequency())?;
match self.past_fixing(fixing_date)? {
Some(fixing) => Ok(fixing),
None => crate::fail!("Missing {} fixing for {}", self.base.name(), first),
}
}
fn past_fixing(&self, fixing_date: Date) -> QlResult<Option<Rate>> {
let (first, _) = inflation_period(fixing_date, self.frequency())?;
Ok(self.base.settings().fixing(&self.base.name(), first))
}
fn add_fixing(&self, fixing_date: Date, value: Rate) -> QlResult<()> {
self.base.add_fixing(fixing_date, value)
}
fn settings(&self) -> &Settings<Date> {
self.base.settings()
}
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl InflationIndex for ZeroInflationIndex {
fn inflation_base(&self) -> &InflationIndexBase {
&self.base
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CpiInterpolationType {
Flat,
Linear,
}
pub enum Cpi {}
impl Cpi {
pub fn lagged_fixing(
index: &ZeroInflationIndex,
date: Date,
observation_lag: Period,
interpolation_type: CpiInterpolationType,
) -> QlResult<Rate> {
let frequency = index.frequency();
let fixing_period = inflation_period(date - observation_lag, frequency)?;
let i0 = index.fixing(fixing_period.0, false)?;
match interpolation_type {
CpiInterpolationType::Flat => Ok(i0),
CpiInterpolationType::Linear => {
let interpolation_period = inflation_period(date, frequency)?;
if date == interpolation_period.0 {
return Ok(i0);
}
let one_day = Period::new(1, TimeUnit::Days);
let i1 = index.fixing(fixing_period.1 + one_day, false)?;
let elapsed = (date - interpolation_period.0) as Rate;
let length = ((interpolation_period.1 + one_day) - interpolation_period.0) as Rate;
Ok(i0 + (i1 - i0) * elapsed / length)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::handle::RelinkableHandle;
use crate::math::interpolations::linear::Linear;
use crate::shared::{shared, shared_mut};
use crate::termstructures::TermStructure;
use crate::termstructures::inflation::inflationtermstructure::InflationTermStructure;
use crate::termstructures::inflation::interpolatedzeroinflationcurve::ZeroInflationCurve;
use crate::time::date::Month::{
April, December, February, January, March, November, October, September,
};
use crate::time::daycounters::actual360::Actual360;
use crate::time::timeunit::TimeUnit;
struct TestInflationIndex {
base: InflationIndexBase,
}
impl TestInflationIndex {
fn new(frequency: Frequency) -> Self {
TestInflationIndex::with_settings(frequency, shared(Settings::<Date>::new()))
}
fn with_settings(frequency: Frequency, settings: Shared<Settings<Date>>) -> Self {
TestInflationIndex {
base: InflationIndexBase::new(
"RPI".into(),
Region::uk(),
false,
frequency,
Period::new(1, TimeUnit::Months),
Currency::gbp(),
settings,
),
}
}
}
#[derive(Default)]
struct Flag {
up: bool,
}
impl Observer for Flag {
fn update(&mut self) {
self.up = true;
}
}
impl Index for TestInflationIndex {
fn name(&self) -> String {
self.base.name()
}
fn fixing_calendar(&self) -> Calendar {
self.base.fixing_calendar()
}
fn is_valid_fixing_date(&self, _fixing_date: Date) -> bool {
true
}
fn fixing(&self, fixing_date: Date, _forecast_todays_fixing: bool) -> QlResult<Rate> {
match self.past_fixing(fixing_date)? {
Some(rate) => Ok(rate),
None => crate::fail!("no fixing for {fixing_date:?}"),
}
}
fn settings(&self) -> &Settings<Date> {
self.base.settings()
}
fn observable(&self) -> &Observable {
self.base.observable()
}
fn add_fixing(&self, fixing_date: Date, value: Rate) -> QlResult<()> {
self.base.add_fixing(fixing_date, value)
}
}
impl InflationIndex for TestInflationIndex {
fn inflation_base(&self) -> &InflationIndexBase {
&self.base
}
}
#[test]
fn name_is_region_plus_family() {
let index = TestInflationIndex::new(Frequency::Monthly);
assert_eq!(index.name(), "UK RPI");
}
#[test]
fn inspectors_round_trip_construction() {
let index = TestInflationIndex::new(Frequency::Monthly);
assert_eq!(index.family_name(), "RPI");
assert_eq!(index.region(), &Region::uk());
assert!(!index.revised());
assert_eq!(index.frequency(), Frequency::Monthly);
assert_eq!(index.availability_lag(), Period::new(1, TimeUnit::Months));
assert_eq!(index.currency().code(), "GBP");
}
#[test]
fn every_date_is_a_valid_fixing_date() {
let index = TestInflationIndex::new(Frequency::Monthly);
let sunday = Date::new(2, Month::December, 2007);
assert!(index.is_valid_fixing_date(sunday));
assert!(index.fixing_calendar().is_business_day(sunday));
}
#[test]
fn monthly_period_is_the_calendar_month() {
let (first, last) = inflation_period(Date::new(14, February, 2007), Frequency::Monthly)
.expect("monthly is a handled frequency");
assert_eq!(first, Date::new(1, February, 2007));
assert_eq!(last, Date::new(28, February, 2007));
}
#[test]
fn monthly_period_ends_on_the_leap_day() {
let (first, last) = inflation_period(Date::new(14, February, 2008), Frequency::Monthly)
.expect("monthly is a handled frequency");
assert_eq!(first, Date::new(1, February, 2008));
assert_eq!(last, Date::new(29, February, 2008));
}
#[test]
fn quarterly_period_aligns_to_the_calendar_quarter() {
let (first, last) = inflation_period(Date::new(15, December, 2007), Frequency::Quarterly)
.expect("quarterly is a handled frequency");
assert_eq!(first, Date::new(1, October, 2007));
assert_eq!(last, Date::new(31, December, 2007));
let (first, last) = inflation_period(Date::new(3, January, 2007), Frequency::Quarterly)
.expect("quarterly is a handled frequency");
assert_eq!(first, Date::new(1, January, 2007));
assert_eq!(last, Date::new(31, March, 2007));
}
#[test]
fn coarser_periods_align_from_january() {
let (first, last) = inflation_period(Date::new(15, December, 2007), Frequency::Semiannual)
.expect("semiannual is a handled frequency");
assert_eq!(first, Date::new(1, Month::July, 2007));
assert_eq!(last, Date::new(31, December, 2007));
let (first, last) = inflation_period(Date::new(15, December, 2007), Frequency::Annual)
.expect("annual is a handled frequency");
assert_eq!(first, Date::new(1, January, 2007));
assert_eq!(last, Date::new(31, December, 2007));
}
#[test]
fn a_non_interpolated_year_fraction_counts_between_period_starts() {
let day_counter = Actual360::new();
let (d1, d2) = (Date::new(14, February, 2007), Date::new(20, April, 2007));
let interpolated =
inflation_year_fraction(Frequency::Monthly, true, &day_counter, d1, d2).unwrap();
let flat =
inflation_year_fraction(Frequency::Monthly, false, &day_counter, d1, d2).unwrap();
assert_eq!(interpolated, day_counter.year_fraction(d1, d2));
assert_eq!(interpolated, 65.0 / 360.0);
assert_eq!(flat, 59.0 / 360.0);
assert_ne!(
flat,
day_counter.year_fraction(d1, Date::new(1, April, 2007))
);
}
#[test]
fn a_year_fraction_quantizes_to_the_frequency() {
let day_counter = Actual360::new();
let (d1, d2) = (Date::new(14, February, 2007), Date::new(20, April, 2007));
let quarterly =
inflation_year_fraction(Frequency::Quarterly, false, &day_counter, d1, d2).unwrap();
assert_eq!(quarterly, 90.0 / 360.0);
assert!(inflation_year_fraction(Frequency::Weekly, false, &day_counter, d1, d2).is_err());
}
#[test]
fn finer_than_monthly_is_rejected() {
assert!(inflation_period(Date::new(15, December, 2007), Frequency::Weekly).is_err());
assert!(inflation_period(Date::new(15, December, 2007), Frequency::Once).is_err());
}
#[test]
fn a_fixing_covers_its_whole_inflation_period() {
let index = TestInflationIndex::new(Frequency::Quarterly);
index
.add_fixing(Date::new(15, December, 2007), 100.0)
.expect("adding a fixing on a quarterly inflation index");
assert!(index.has_historical_fixing(Date::new(1, October, 2007)));
assert!(index.has_historical_fixing(Date::new(30, November, 2007)));
assert!(index.has_historical_fixing(Date::new(31, December, 2007)));
assert!(!index.has_historical_fixing(Date::new(30, September, 2007)));
assert!(!index.has_historical_fixing(Date::new(1, January, 2008)));
}
#[test]
fn last_fixing_date_is_the_end_of_the_published_period() {
let index = TestInflationIndex::new(Frequency::Quarterly);
index
.add_fixing(Date::new(15, December, 2007), 100.0)
.expect("adding a fixing on a quarterly inflation index");
assert_eq!(
index.settings().last_fixing_date("UK RPI"),
Some(Date::new(31, December, 2007))
);
}
#[test]
fn last_fixing_date_is_none_without_a_history() {
let index = TestInflationIndex::new(Frequency::Quarterly);
assert_eq!(index.settings().last_fixing_date("UK RPI"), None);
}
#[test]
fn the_index_re_broadcasts_its_dependencies() {
let settings = shared(Settings::<Date>::new());
let index = TestInflationIndex::with_settings(Frequency::Monthly, settings.clone());
let flag = shared_mut(Flag::default());
index
.observable()
.register_observer(&(flag.clone() as SharedMut<dyn Observer>));
settings.set_evaluation_date(Date::new(3, December, 2007));
assert!(flag.borrow().up);
flag.borrow_mut().up = false;
let curve = Observable::new();
curve.register_observer(&index.inflation_base().observer());
curve.notify_observers();
assert!(flag.borrow().up);
}
fn a_zero_index(frequency: Frequency, settings: Shared<Settings<Date>>) -> ZeroInflationIndex {
ZeroInflationIndex::new(
"RPI".into(),
Region::uk(),
false,
frequency,
Period::new(1, TimeUnit::Months),
Currency::gbp(),
settings,
)
}
fn an_april_2024_zero_index() -> ZeroInflationIndex {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(Date::new(10, April, 2024));
let index = a_zero_index(Frequency::Monthly, settings);
for (date, value) in [
(Date::new(1, December, 2023), 100.0),
(Date::new(1, January, 2024), 100.1),
(Date::new(1, February, 2024), 100.2),
] {
index
.add_fixing(date, value)
.expect("adding a published figure");
}
index
}
#[test]
fn a_zero_fixing_covers_its_whole_inflation_period() {
let index = a_zero_index(Frequency::Quarterly, shared(Settings::<Date>::new()));
index
.add_fixing(Date::new(15, December, 2007), 100.0)
.expect("adding a fixing on a quarterly zero inflation index");
assert!(index.has_historical_fixing(Date::new(30, November, 2007)));
assert!(index.has_historical_fixing(Date::new(31, December, 2007)));
assert!(!index.has_historical_fixing(Date::new(30, September, 2007)));
assert!(!index.has_historical_fixing(Date::new(1, January, 2008)));
}
#[test]
fn a_past_fixing_is_read_at_the_start_of_its_period() {
let index = a_zero_index(Frequency::Quarterly, shared(Settings::<Date>::new()));
index
.add_fixings([(Date::new(1, October, 2007), 100.0)])
.expect("recording a single raw entry through the Index default");
assert_eq!(
index
.past_fixing(Date::new(20, December, 2007))
.expect("every date is a valid fixing date"),
Some(100.0)
);
}
#[test]
fn a_fixing_before_the_horizon_is_read_from_history() {
let index = an_april_2024_zero_index();
let february = Date::new(1, February, 2024);
assert!(!index.needs_forecast(february).expect("a monthly index"));
assert_eq!(
index
.fixing(february, false)
.expect("February 2024 is published"),
100.2
);
}
#[test]
fn a_missing_fixing_before_the_horizon_is_an_error() {
let index = an_april_2024_zero_index();
let november = Date::new(1, November, 2023);
assert!(!index.needs_forecast(november).expect("a monthly index"));
let error = index
.fixing(november, false)
.expect_err("November 2023 was never published");
assert!(error.to_string().contains("Missing"), "err was: {error}");
}
#[test]
fn a_fixing_inside_the_horizon_forecasts_only_while_absent() {
let index = an_april_2024_zero_index();
let march = Date::new(1, March, 2024);
assert!(index.needs_forecast(march).expect("a monthly index"));
let error = index
.fixing(march, false)
.expect_err("March 2024 has no figure yet and no curve to forecast off");
assert!(
error.to_string().contains("empty Handle"),
"err was: {error}"
);
index
.add_fixing(march, 100.3)
.expect("March 2024 gets published");
assert!(!index.needs_forecast(march).expect("a monthly index"));
assert_eq!(
index.fixing(march, false).expect("March 2024 is published"),
100.3
);
}
#[test]
fn a_fixing_beyond_the_horizon_forecasts_even_when_stored() {
let index = an_april_2024_zero_index();
let april = Date::new(1, April, 2024);
assert!(index.needs_forecast(april).expect("a monthly index"));
index
.add_fixing(april, 100.4)
.expect("recording a figure ahead of its publication");
assert!(index.needs_forecast(april).expect("a monthly index"));
let error = index
.fixing(april, false)
.expect_err("April 2024 is beyond the publication horizon");
assert!(
error.to_string().contains("empty Handle"),
"err was: {error}"
);
}
#[test]
fn the_last_fixing_date_is_the_start_of_the_published_period() {
let index = a_zero_index(Frequency::Quarterly, shared(Settings::<Date>::new()));
index
.add_fixing(Date::new(15, December, 2007), 100.0)
.expect("adding a fixing on a quarterly zero inflation index");
assert_eq!(
index.last_fixing_date().expect("the index has a history"),
Date::new(1, October, 2007)
);
}
#[test]
fn the_last_fixing_date_of_an_empty_index_is_an_error() {
let index = a_zero_index(Frequency::Quarterly, shared(Settings::<Date>::new()));
let error = index
.last_fixing_date()
.expect_err("an index with no history has no last fixing date");
assert!(
error.to_string().contains("no fixings stored"),
"err was: {error}"
);
}
#[test]
fn last_fixing_date_is_case_insensitive() {
let index = TestInflationIndex::new(Frequency::Quarterly);
index
.add_fixing(Date::new(15, April, 2007), 100.0)
.expect("adding a fixing on a quarterly inflation index");
let expected = Some(Date::new(30, Month::June, 2007));
assert_eq!(index.settings().last_fixing_date("UK RPI"), expected);
assert_eq!(index.settings().last_fixing_date("uk rpi"), expected);
}
const BASE_FIXING: Rate = 100.0;
fn today() -> Date {
Date::new(15, January, 2022)
}
fn curve_base_date() -> Date {
Date::new(1, November, 2021)
}
fn a_curve(base_date: Date, rates: Vec<Rate>) -> Shared<ZeroInflationCurve> {
shared(
ZeroInflationCurve::new(
today(),
vec![
base_date,
Date::new(1, January, 2023),
Date::new(1, January, 2025),
],
rates,
Frequency::Monthly,
Actual360::new(),
Linear,
)
.expect("a well-formed zero inflation curve"),
)
}
fn an_index_on(curve: &Shared<ZeroInflationCurve>) -> ZeroInflationIndex {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(today());
a_zero_index(Frequency::Monthly, settings)
.with_term_structure(Handle::new(
Shared::clone(curve) as Shared<dyn ZeroInflationTermStructure>
))
}
#[test]
fn a_forecast_compounds_the_base_fixing_by_the_curve_zero_rate() {
let curve = a_curve(curve_base_date(), vec![0.02, 0.05, 0.06]);
let index = an_index_on(&curve);
index
.add_fixing(curve_base_date(), BASE_FIXING)
.expect("seeding the base-date period");
let mid_march = Date::new(15, March, 2022);
let period_start = Date::new(1, March, 2022);
assert!(index.needs_forecast(mid_march).expect("a monthly index"));
let t = curve.time_from_reference(period_start).unwrap();
assert_eq!(t, 45.0 / 360.0);
assert!(t > 0.0);
let (t_lo, t_hi) = (curve.times()[0], curve.times()[1]);
let (r_lo, r_hi) = (curve.rates()[0], curve.rates()[1]);
let z1 = r_lo + (t - t_lo) / (t_hi - t_lo) * (r_hi - r_lo);
let t1 = (period_start - curve_base_date()) as Time / 360.0;
assert_eq!(t1, 120.0 / 360.0);
let expected = BASE_FIXING * (1.0 + z1).powf(t1);
let forecast = index
.fixing(mid_march, false)
.expect("March 2022 is forecast");
assert!(
(forecast - expected).abs() < 1.0e-10,
"forecast was {forecast}"
);
let unquantized = curve.time_from_reference(mid_march).unwrap();
let z_unquantized = r_lo + (unquantized - t_lo) / (t_hi - t_lo) * (r_hi - r_lo);
assert!((z1 - z_unquantized).abs() > 1.0e-4);
}
#[test]
fn every_date_in_a_period_forecasts_the_same_figure() {
let curve = a_curve(curve_base_date(), vec![0.02, 0.05, 0.06]);
let index = an_index_on(&curve);
index
.add_fixing(curve_base_date(), BASE_FIXING)
.expect("seeding the base-date period");
let first = index.fixing(Date::new(1, March, 2022), false).unwrap();
assert_eq!(
index.fixing(Date::new(15, March, 2022), false).unwrap(),
first
);
assert_eq!(
index.fixing(Date::new(31, March, 2022), false).unwrap(),
first
);
assert_ne!(
index.fixing(Date::new(1, April, 2022), false).unwrap(),
first
);
}
#[test]
fn a_forecast_needs_the_fixing_at_the_curve_base_date() {
let curve = a_curve(Date::new(1, January, 2022), vec![0.02, 0.05, 0.06]);
let index = an_index_on(&curve);
assert!(index.needs_forecast(curve.base_date()).unwrap());
let error = index
.fixing(Date::new(15, March, 2022), false)
.expect_err("the base-date figure cannot have been published");
assert!(
error.to_string().contains("index fixing at base date"),
"err was: {error}"
);
}
#[test]
fn a_zero_rate_at_or_below_minus_one_forecasts_zero() {
let curve = a_curve(curve_base_date(), vec![-1.5, -0.9, -0.8]);
let index = an_index_on(&curve);
index
.add_fixing(curve_base_date(), BASE_FIXING)
.expect("seeding the base-date period");
let march = Date::new(15, March, 2022);
assert!(curve.zero_rate_date(march, false).unwrap() <= -1.0);
assert_eq!(index.fixing(march, false).unwrap(), 0.0);
}
#[test]
fn the_index_re_broadcasts_a_curve_relink() {
let curve = a_curve(curve_base_date(), vec![0.02, 0.05, 0.06]);
let handle: RelinkableHandle<dyn ZeroInflationTermStructure> = RelinkableHandle::empty();
let settings = shared(Settings::<Date>::new());
let index = a_zero_index(Frequency::Monthly, settings).with_term_structure(handle.handle());
let flag = shared_mut(Flag::default());
index
.observable()
.register_observer(&(flag.clone() as SharedMut<dyn Observer>));
handle.link_to(Shared::clone(&curve) as Shared<dyn ZeroInflationTermStructure>);
assert!(flag.borrow().up);
}
#[test]
fn a_clone_keeps_the_indexs_identity() {
let curve = a_curve(curve_base_date(), vec![0.02, 0.05, 0.06]);
let index = an_index_on(&curve);
let copy = index.clone_linked_to(Handle::empty());
assert_eq!(copy.name(), index.name());
assert_eq!(copy.family_name(), index.family_name());
assert_eq!(copy.region(), index.region());
assert_eq!(copy.revised(), index.revised());
assert_eq!(copy.frequency(), index.frequency());
assert_eq!(copy.availability_lag(), index.availability_lag());
assert_eq!(copy.currency().code(), index.currency().code());
}
#[test]
fn a_clone_shares_the_originals_fixings() {
let curve = a_curve(curve_base_date(), vec![0.02, 0.05, 0.06]);
let index = an_index_on(&curve);
let copy = index.clone_linked_to(Handle::empty());
index
.add_fixing(curve_base_date(), BASE_FIXING)
.expect("seeding the base-date period");
assert_eq!(
copy.past_fixing(Date::new(20, November, 2021))
.expect("every date is a valid fixing date"),
Some(BASE_FIXING)
);
assert_eq!(
copy.last_fixing_date().expect("the shared history"),
curve_base_date()
);
copy.add_fixing(Date::new(1, December, 2021), 101.0)
.expect("publishing through the copy");
assert_eq!(
index
.past_fixing(Date::new(15, December, 2021))
.expect("every date is a valid fixing date"),
Some(101.0)
);
}
#[test]
fn a_clone_forecasts_off_its_own_curve() {
let index = an_index_on(&a_curve(curve_base_date(), vec![0.02, 0.05, 0.06]));
index
.add_fixing(curve_base_date(), BASE_FIXING)
.expect("seeding the base-date period");
let slower = a_curve(curve_base_date(), vec![0.01, 0.02, 0.03]);
let copy = index.clone_linked_to(Handle::new(
slower as Shared<dyn ZeroInflationTermStructure>,
));
let march = Date::new(15, March, 2022);
let original = index.fixing(march, false).expect("March 2022 is forecast");
let cloned = copy.fixing(march, false).expect("March 2022 is forecast");
assert!(original > BASE_FIXING);
assert!(cloned > BASE_FIXING);
assert!(
original - cloned > 0.5,
"the two curves gave {original} and {cloned}"
);
}
#[test]
fn a_clone_observes_only_its_own_handle() {
let curve = a_curve(curve_base_date(), vec![0.02, 0.05, 0.06]);
let handle: RelinkableHandle<dyn ZeroInflationTermStructure> = RelinkableHandle::empty();
let settings = shared(Settings::<Date>::new());
let index = a_zero_index(Frequency::Monthly, settings);
let copy = index.clone_linked_to(handle.handle());
let on_original = shared_mut(Flag::default());
index
.observable()
.register_observer(&(on_original.clone() as SharedMut<dyn Observer>));
let on_copy = shared_mut(Flag::default());
copy.observable()
.register_observer(&(on_copy.clone() as SharedMut<dyn Observer>));
handle.link_to(Shared::clone(&curve) as Shared<dyn ZeroInflationTermStructure>);
assert!(on_copy.borrow().up, "the copy observes the handle it took");
assert!(!on_original.borrow().up, "the original is a separate index");
}
}