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::{
YoYInflationTermStructure, 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
}
}
pub struct YoYInflationIndex {
base: InflationIndexBase,
term_structure: Handle<dyn YoYInflationTermStructure>,
ratio: bool,
underlying: Option<Shared<ZeroInflationIndex>>,
}
impl YoYInflationIndex {
pub fn new(
family_name: String,
region: Region,
revised: bool,
frequency: Frequency,
availability_lag: Period,
currency: Currency,
settings: Shared<Settings<Date>>,
) -> Self {
YoYInflationIndex {
base: InflationIndexBase::new(
family_name,
region,
revised,
frequency,
availability_lag,
currency,
settings,
),
term_structure: Handle::empty(),
ratio: false,
underlying: None,
}
}
pub fn from_underlying(underlying: Shared<ZeroInflationIndex>) -> Self {
let base = InflationIndexBase::new(
format!("YYR_{}", underlying.family_name()),
underlying.region().clone(),
underlying.revised(),
underlying.frequency(),
underlying.availability_lag(),
underlying.currency().clone(),
Shared::clone(underlying.inflation_base().settings()),
);
underlying.observable().register_observer(&base.observer());
YoYInflationIndex {
base,
term_structure: Handle::empty(),
ratio: true,
underlying: Some(underlying),
}
}
pub fn with_term_structure(
self,
term_structure: Handle<dyn YoYInflationTermStructure>,
) -> YoYInflationIndex {
term_structure.register_observer(&self.base.observer());
YoYInflationIndex {
term_structure,
..self
}
}
pub fn ratio(&self) -> bool {
self.ratio
}
pub fn underlying_index(&self) -> Option<&Shared<ZeroInflationIndex>> {
self.underlying.as_ref()
}
pub fn yoy_inflation_term_structure(&self) -> &Handle<dyn YoYInflationTermStructure> {
&self.term_structure
}
pub fn last_fixing_date(&self) -> QlResult<Date> {
if let Some(underlying) = &self.underlying {
return underlying.last_fixing_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 frequency = self.frequency();
let latest_needed_date = inflation_period(fixing_date, frequency)?.0;
if let Some(underlying) = &self.underlying {
return underlying.needs_forecast(latest_needed_date);
}
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 latest_possible = inflation_period(today - self.availability_lag(), frequency)?;
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 (first_date_in_period, _) = inflation_period(fixing_date, self.frequency())?;
self.term_structure
.current_link()?
.yoy_rate_date(first_date_in_period, false)
}
pub fn clone_linked_to(
&self,
term_structure: Handle<dyn YoYInflationTermStructure>,
) -> YoYInflationIndex {
let copy = match &self.underlying {
Some(underlying) => YoYInflationIndex::from_underlying(Shared::clone(underlying)),
None => YoYInflationIndex::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()),
),
};
copy.with_term_structure(term_structure)
}
}
impl Index for YoYInflationIndex {
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 underlying = match &self.underlying {
Some(underlying) => underlying,
None => {
let (first, _) = inflation_period(fixing_date, self.frequency())?;
return Ok(self.base.settings().fixing(&self.base.name(), first));
}
};
let no_lag = Period::new(0, TimeUnit::Months);
let interpolation = CpiInterpolationType::Flat;
let past = Cpi::lagged_fixing(underlying, fixing_date, no_lag, interpolation)?;
let previous = Cpi::lagged_fixing(
underlying,
fixing_date - Period::new(1, TimeUnit::Years),
no_lag,
interpolation,
)?;
Ok(Some(past / previous - 1.0))
}
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 YoYInflationIndex {
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)
}
}
}
pub fn lagged_yoy_rate(
yoy_index: &YoYInflationIndex,
date: Date,
observation_lag: Period,
interpolation_type: CpiInterpolationType,
) -> QlResult<Rate> {
let frequency = yoy_index.frequency();
match interpolation_type {
CpiInterpolationType::Flat => {
let fixing_period = inflation_period(date - observation_lag, frequency)?;
yoy_index.fixing(fixing_period.0, false)
}
CpiInterpolationType::Linear => {
if let Some(underlying) = yoy_index.underlying_index()
&& !yoy_index.needs_forecast(date)?
{
let z1 =
Cpi::lagged_fixing(underlying, date, observation_lag, interpolation_type)?;
let z0 = Cpi::lagged_fixing(
underlying,
date - Period::new(1, TimeUnit::Years),
observation_lag,
interpolation_type,
)?;
return Ok(z1 / z0 - 1.0);
}
let fixing_period = inflation_period(date - observation_lag, frequency)?;
let interpolation_period = inflation_period(date, frequency)?;
let y0 = yoy_index.fixing(fixing_period.0, false)?;
if date == interpolation_period.0 {
return Ok(y0);
}
let one_day = Period::new(1, TimeUnit::Days);
let y1 = yoy_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(y0 + (y1 - y0) * 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::businessdayconvention::BusinessDayConvention;
use crate::time::calendars::unitedkingdom::{Market, UnitedKingdom};
use crate::time::date::Month::{
April, August, December, February, January, June, March, May, November, October, September,
};
use crate::time::daycounters::actual360::Actual360;
use crate::time::schedule::MakeSchedule;
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,
None,
)
.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");
}
fn a_yoy_ratio_index(
settings: Shared<Settings<Date>>,
) -> (Shared<ZeroInflationIndex>, YoYInflationIndex) {
let underlying = shared(a_zero_index(Frequency::Monthly, settings));
let ratio = YoYInflationIndex::from_underlying(Shared::clone(&underlying));
(underlying, ratio)
}
fn a_quoted_yoy_index(settings: Shared<Settings<Date>>) -> YoYInflationIndex {
YoYInflationIndex::new(
"YY_RPI".into(),
Region::uk(),
false,
Frequency::Monthly,
Period::new(1, TimeUnit::Months),
Currency::gbp(),
settings,
)
}
#[test]
fn a_quoted_yoy_index_states_its_own_metadata() {
let index = a_quoted_yoy_index(shared(Settings::<Date>::new()));
assert_eq!(index.name(), "UK YY_RPI");
assert_eq!(index.frequency(), Frequency::Monthly);
assert!(!index.revised());
assert!(!index.ratio());
assert!(index.underlying_index().is_none());
assert_eq!(index.availability_lag(), Period::new(1, TimeUnit::Months));
}
#[test]
fn a_ratio_yoy_index_inherits_the_underlyings_metadata() {
let (underlying, index) = a_yoy_ratio_index(shared(Settings::<Date>::new()));
assert_eq!(index.name(), "UK YYR_RPI");
assert_eq!(index.family_name(), "YYR_RPI");
assert_eq!(index.frequency(), underlying.frequency());
assert_eq!(index.revised(), underlying.revised());
assert!(index.ratio());
assert_eq!(index.availability_lag(), underlying.availability_lag());
assert_eq!(index.currency().code(), underlying.currency().code());
}
const YOY_FIX_DATA: [Rate; 31] = [
189.9, 189.9, 189.6, 190.5, 191.6, 192.0, 192.2, 192.2, 192.6, 193.1, 193.3, 193.6, 194.1,
193.4, 194.2, 195.0, 196.5, 197.7, 198.5, 198.5, 199.2, 200.1, 200.4, 201.1, 202.7, 201.6,
203.1, 204.4, 205.4, 206.2, 207.3,
];
#[test]
fn a_ratio_fixing_is_the_underlyings_year_on_year_ratio() {
let settings = shared(Settings::<Date>::new());
let evaluation_date = UnitedKingdom::new(Market::Settlement).adjust(
Date::new(13, August, 2007),
BusinessDayConvention::Following,
);
settings.set_evaluation_date(evaluation_date);
let (underlying, index) = a_yoy_ratio_index(settings);
let schedule = MakeSchedule::new()
.from(Date::new(1, January, 2005))
.to(Date::new(13, August, 2007))
.with_tenor(Period::new(1, TimeUnit::Months))
.with_calendar(UnitedKingdom::new(Market::Settlement))
.with_convention(BusinessDayConvention::ModifiedFollowing)
.build();
let dates = schedule.dates();
assert!(dates.len() >= YOY_FIX_DATA.len());
let month_of = |date: Date| {
inflation_period(date, index.frequency())
.expect("a monthly index")
.0
};
assert_eq!(month_of(dates[0]), month_of(dates[1]));
for i in 13..YOY_FIX_DATA.len() {
assert_eq!(
month_of(dates[i]),
month_of(dates[i - 12]) + Period::new(1, TimeUnit::Years),
"figures {i} and {} are not a year apart",
i - 12
);
}
for (date, value) in dates.iter().zip(YOY_FIX_DATA) {
underlying
.add_fixing(*date, value)
.expect("adding a published figure");
}
let (_, latest_possible_end) = inflation_period(
evaluation_date - index.availability_lag(),
index.frequency(),
)
.expect("a monthly index");
let horizon = latest_possible_end + 1 - Period::new(2, TimeUnit::Months);
assert_eq!(horizon, Date::new(1, June, 2007));
for i in 13..YOY_FIX_DATA.len() {
let (first, last) =
inflation_period(dates[i], index.frequency()).expect("a monthly index");
let expected = YOY_FIX_DATA[i] / YOY_FIX_DATA[i - 12] - 1.0;
for day in (0..=(last - first)).map(|offset| first + offset) {
if day < horizon {
let calculated = index
.fixing(day, false)
.expect("both periods are published");
assert!(
(calculated - expected).abs() < 1e-8,
"{calculated} at {day}, should be {expected}"
);
}
}
}
}
#[test]
fn a_quoted_yoy_index_forecasts_beyond_the_publication_horizon() {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(Date::new(10, April, 2024));
let index = a_quoted_yoy_index(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 rate");
}
assert_eq!(
index.last_fixing_date().expect("the index has a history"),
Date::new(1, February, 2024)
);
assert_eq!(
index
.fixing(Date::new(15, January, 2024), false)
.expect("January 2024 is published"),
100.1
);
assert_eq!(
index
.fixing(Date::new(15, February, 2024), false)
.expect("February 2024 is published"),
100.2
);
index
.add_fixing(Date::new(1, March, 2024), 100.3)
.expect("March 2024 gets published");
assert_eq!(
index.last_fixing_date().expect("the index has a history"),
Date::new(1, March, 2024)
);
assert_eq!(
index
.fixing(Date::new(15, February, 2024), false)
.expect("February 2024 is still published"),
100.2
);
index
.add_fixing(Date::new(1, April, 2024), 100.4)
.expect("recording a rate ahead of its publication");
let error = index
.fixing(Date::new(1, April, 2024), false)
.expect_err("April 2024 is beyond the publication horizon");
assert!(
error.to_string().contains("empty Handle"),
"err was: {error}"
);
}
#[test]
fn a_missing_quoted_rate_before_the_horizon_is_an_error() {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(Date::new(10, April, 2024));
let index = a_quoted_yoy_index(settings);
let error = index
.fixing(Date::new(15, December, 2023), false)
.expect_err("December 2023 was never published");
assert!(
error.to_string().contains("Missing UK YY_RPI fixing"),
"err was: {error}"
);
}
#[test]
fn a_ratio_yoy_index_defers_the_forecast_decision_to_its_underlying() {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(Date::new(10, April, 2024));
let (underlying, index) = a_yoy_ratio_index(settings);
for (date, value) in [
(Date::new(1, December, 2022), 98.0),
(Date::new(1, January, 2023), 98.1),
(Date::new(1, February, 2023), 98.2),
(Date::new(1, March, 2023), 98.3),
(Date::new(1, December, 2023), 100.0),
(Date::new(1, January, 2024), 100.1),
(Date::new(1, February, 2024), 100.2),
] {
underlying
.add_fixing(date, value)
.expect("adding a published figure");
}
assert_eq!(
index
.last_fixing_date()
.expect("the underlying has a history"),
Date::new(1, February, 2024)
);
assert!(
(index
.fixing(Date::new(15, January, 2024), false)
.expect("both January periods are published")
- (100.1 / 98.1 - 1.0))
.abs()
< 1e-8
);
assert!(
(index
.fixing(Date::new(15, February, 2024), false)
.expect("both February periods are published")
- (100.2 / 98.2 - 1.0))
.abs()
< 1e-8
);
underlying
.add_fixing(Date::new(1, March, 2024), 100.3)
.expect("March 2024 gets published");
assert_eq!(
index
.last_fixing_date()
.expect("the underlying has a history"),
Date::new(1, March, 2024)
);
underlying
.add_fixing(Date::new(1, April, 2024), 100.4)
.expect("recording a figure ahead of its publication");
let error = index
.fixing(Date::new(1, April, 2024), false)
.expect_err("April 2024 is beyond the publication horizon");
assert!(
error.to_string().contains("empty Handle"),
"err was: {error}"
);
}
fn a_quoted_yoy_with_2021_rates() -> YoYInflationIndex {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(Date::new(10, February, 2022));
let index = a_quoted_yoy_index(settings);
for (date, value) in [
(Date::new(1, November, 2020), 0.02935),
(Date::new(1, December, 2020), 0.02954),
(Date::new(1, January, 2021), 0.02946),
(Date::new(1, February, 2021), 0.02960),
(Date::new(1, March, 2021), 0.02969),
] {
index
.add_fixing(date, value)
.expect("adding a published rate");
}
index
}
fn a_ratio_yoy_with_2021_fixings() -> YoYInflationIndex {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(Date::new(10, February, 2022));
let (underlying, index) = a_yoy_ratio_index(settings);
for (date, value) in [
(Date::new(1, November, 2019), 291.0),
(Date::new(1, December, 2019), 291.9),
(Date::new(1, January, 2020), 290.6),
(Date::new(1, February, 2020), 292.0),
(Date::new(1, March, 2020), 292.6),
(Date::new(1, November, 2020), 293.5),
(Date::new(1, December, 2020), 295.4),
(Date::new(1, January, 2021), 294.6),
(Date::new(1, February, 2021), 296.0),
(Date::new(1, March, 2021), 296.9),
] {
underlying
.add_fixing(date, value)
.expect("adding a published figure");
}
index
}
fn lagged_yoy_rate(
index: &YoYInflationIndex,
date: Date,
interpolation_type: CpiInterpolationType,
) -> QlResult<Rate> {
Cpi::lagged_yoy_rate(
index,
date,
Period::new(3, TimeUnit::Months),
interpolation_type,
)
}
#[test]
fn a_flat_yoy_observation_reads_the_lagged_period() {
let index = a_quoted_yoy_with_2021_rates();
for (date, expected) in [
(Date::new(10, February, 2021), 0.02935),
(Date::new(25, June, 2021), 0.02969),
] {
let calculated = lagged_yoy_rate(&index, date, CpiInterpolationType::Flat)
.expect("the observed period is published");
assert!(
(calculated - expected).abs() < 1e-8,
"{calculated} at {date}"
);
}
}
#[test]
fn a_linear_yoy_observation_interpolates_the_quoted_rates() {
let index = a_quoted_yoy_with_2021_rates();
for (date, expected) in [
(
Date::new(10, February, 2021),
0.02935 * (19.0 / 28.0) + 0.02954 * (9.0 / 28.0),
),
(
Date::new(12, May, 2021),
0.02960 * (20.0 / 31.0) + 0.02969 * (11.0 / 31.0),
),
] {
let calculated = lagged_yoy_rate(&index, date, CpiInterpolationType::Linear)
.expect("both observed periods are published");
assert!(
(calculated - expected).abs() < 1e-8,
"{calculated} at {date}"
);
}
}
#[test]
fn a_linear_yoy_observation_propagates_a_missing_quoted_rate() {
let index = a_quoted_yoy_with_2021_rates();
let error = lagged_yoy_rate(
&index,
Date::new(25, June, 2021),
CpiInterpolationType::Linear,
)
.expect_err("April 2021 was never published");
assert!(
error.to_string().contains("Missing UK YY_RPI fixing"),
"err was: {error}"
);
}
#[test]
fn a_linear_yoy_observation_on_a_period_start_skips_the_second_rate() {
let index = a_quoted_yoy_with_2021_rates();
let calculated = lagged_yoy_rate(
&index,
Date::new(1, June, 2021),
CpiInterpolationType::Linear,
)
.expect("the special case never reads April");
assert!((calculated - 0.02969).abs() < 1e-8, "{calculated}");
}
#[test]
fn a_flat_yoy_observation_on_a_ratio_index_divides_the_lagged_levels() {
let index = a_ratio_yoy_with_2021_fixings();
for (date, expected) in [
(Date::new(10, February, 2021), 293.5 / 291.0 - 1.0),
(Date::new(25, June, 2021), 296.9 / 292.6 - 1.0),
] {
let calculated = lagged_yoy_rate(&index, date, CpiInterpolationType::Flat)
.expect("both observed periods are published");
assert!(
(calculated - expected).abs() < 1e-8,
"{calculated} at {date}"
);
}
}
#[test]
fn a_linear_yoy_observation_on_a_ratio_index_interpolates_before_dividing() {
let index = a_ratio_yoy_with_2021_fixings();
for (date, expected) in [
(
Date::new(10, February, 2021),
(293.5 * (19.0 / 28.0) + 295.4 * (9.0 / 28.0))
/ (291.0 * (20.0 / 29.0) + 291.9 * (9.0 / 29.0))
- 1.0,
),
(
Date::new(12, May, 2021),
(296.0 * (20.0 / 31.0) + 296.9 * (11.0 / 31.0))
/ (292.0 * (20.0 / 31.0) + 292.6 * (11.0 / 31.0))
- 1.0,
),
] {
let calculated = lagged_yoy_rate(&index, date, CpiInterpolationType::Linear)
.expect("both observed periods are published");
assert!(
(calculated - expected).abs() < 1e-8,
"{calculated} at {date}"
);
}
}
#[test]
fn a_linear_yoy_ratio_propagates_a_missing_underlying_fixing() {
let index = a_ratio_yoy_with_2021_fixings();
let error = lagged_yoy_rate(
&index,
Date::new(25, June, 2021),
CpiInterpolationType::Linear,
)
.expect_err("April 2021 was never published");
assert!(
error.to_string().contains("Missing UK RPI fixing"),
"err was: {error}"
);
}
#[test]
fn a_linear_yoy_ratio_on_a_period_start_skips_both_second_fixings() {
let index = a_ratio_yoy_with_2021_fixings();
let calculated = lagged_yoy_rate(
&index,
Date::new(1, June, 2021),
CpiInterpolationType::Linear,
)
.expect("the special case never reads April");
assert!(
(calculated - (296.9 / 292.6 - 1.0)).abs() < 1e-8,
"{calculated}"
);
}
}