use crate::errors::QlResult;
use crate::math::interpolations::linear::Linear;
use crate::math::interpolations::{Interpolation, Interpolator};
use crate::patterns::observable::{AsObservable, Observable};
use crate::shared::Shared;
use crate::termstructures::inflation::inflationtermstructure::{
InflationTermStructure, InflationTermStructureBase, ZeroInflationTermStructure,
};
use crate::termstructures::inflation::seasonality::Seasonality;
use crate::termstructures::interpolatedcurve::InterpolatedCurve;
use crate::termstructures::{TermStructure, TermStructureBase};
use crate::time::date::Date;
use crate::time::daycounter::DayCounter;
use crate::time::frequency::Frequency;
use crate::types::{Rate, Real, Time};
use crate::{fail, require};
pub struct InterpolatedZeroInflationCurve<I: Interpolator> {
inflation: InflationTermStructureBase,
curve: InterpolatedCurve<I>,
dates: Vec<Date>,
}
pub type ZeroInflationCurve = InterpolatedZeroInflationCurve<Linear>;
impl<I: Interpolator> InterpolatedZeroInflationCurve<I> {
pub fn new(
reference_date: Date,
dates: Vec<Date>,
rates: Vec<Rate>,
frequency: Frequency,
day_counter: DayCounter,
interpolator: I,
seasonality: Option<Shared<dyn Seasonality>>,
) -> QlResult<InterpolatedZeroInflationCurve<I>> {
require!(dates.len() > 1, "too few dates: {}", dates.len());
require!(
rates.len() == dates.len(),
"indices/dates count mismatch: {} vs {}",
rates.len(),
dates.len()
);
for &rate in &rates[1..] {
if rate <= -1.0 || rate.is_nan() {
fail!("zero inflation data < -100 %");
}
}
let base_date = dates[0];
let times = InterpolatedCurve::<I>::times_from_dates(&dates, reference_date, &day_counter)?;
let mut curve = InterpolatedCurve::new(times, rates, interpolator);
curve.setup_interpolation()?;
let inflation = InflationTermStructureBase::with_reference_date(
reference_date,
base_date,
frequency,
Some(day_counter),
None,
seasonality,
);
let curve = InterpolatedZeroInflationCurve {
inflation,
curve,
dates,
};
curve.check_seasonality()?;
Ok(curve)
}
pub fn times(&self) -> &[Time] {
self.curve.times()
}
pub fn dates(&self) -> &[Date] {
&self.dates
}
pub fn data(&self) -> &[Real] {
self.curve.data()
}
pub fn rates(&self) -> &[Rate] {
self.curve.data()
}
pub fn nodes(&self) -> Vec<(Date, Rate)> {
self.dates
.iter()
.copied()
.zip(self.curve.data().iter().copied())
.collect()
}
fn last_date(&self) -> Date {
*self
.dates
.last()
.expect("construction rejected empty dates")
}
}
impl<I: Interpolator> AsObservable for InterpolatedZeroInflationCurve<I> {
fn observable(&self) -> &Observable {
self.inflation.term_structure_base().observable()
}
}
impl<I: Interpolator> TermStructure for InterpolatedZeroInflationCurve<I> {
fn base(&self) -> &TermStructureBase {
self.inflation.term_structure_base()
}
fn max_date(&self) -> Date {
self.curve.max_date().unwrap_or_else(|| self.last_date())
}
}
impl<I: Interpolator> InflationTermStructure for InterpolatedZeroInflationCurve<I> {
fn inflation_base(&self) -> &InflationTermStructureBase {
&self.inflation
}
fn as_inflation_term_structure(&self) -> &dyn InflationTermStructure {
self
}
}
impl<I: Interpolator> ZeroInflationTermStructure for InterpolatedZeroInflationCurve<I> {
fn zero_rate_impl(&self, t: Time) -> QlResult<Rate> {
self.curve.interpolation()?.value(t)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shared::shared;
use crate::termstructures::inflation::seasonality::MultiplicativePriceSeasonality;
use crate::time::date::Month;
use crate::time::daycounters::actual360::Actual360;
use crate::time::period::Period;
use crate::time::timeunit::TimeUnit;
fn today() -> Date {
Date::new(27, Month::January, 2022)
}
fn base_date() -> Date {
Date::new(1, Month::December, 2021)
}
fn sample_dates() -> Vec<Date> {
let today = today();
vec![
base_date(),
today + 7,
today + 14,
today + Period::new(1, TimeUnit::Months),
today + Period::new(2, TimeUnit::Months),
today + Period::new(3, TimeUnit::Months),
today + Period::new(6, TimeUnit::Months),
today + Period::new(1, TimeUnit::Years),
today + Period::new(2, TimeUnit::Years),
today + Period::new(5, TimeUnit::Years),
today + Period::new(10, TimeUnit::Years),
]
}
fn sample_rates() -> Vec<Rate> {
vec![
0.01, 0.01, 0.011, 0.012, 0.013, 0.015, 0.018, 0.02, 0.025, 0.03, 0.03,
]
}
fn build(dates: Vec<Date>, rates: Vec<Rate>) -> QlResult<ZeroInflationCurve> {
ZeroInflationCurve::new(
today(),
dates,
rates,
Frequency::Monthly,
Actual360::new(),
Linear,
None,
)
}
fn sample() -> ZeroInflationCurve {
build(sample_dates(), sample_rates()).unwrap()
}
#[test]
fn the_constructor_installs_and_gates_a_seasonality() {
fn built(count: usize) -> QlResult<ZeroInflationCurve> {
let seasonality = shared(
MultiplicativePriceSeasonality::new(
Date::new(31, Month::January, 2022),
Frequency::Monthly,
(0..count).map(|i| 1.0 + i as Rate / 1000.0).collect(),
)
.expect("a whole multiple of twelve factors"),
) as Shared<dyn Seasonality>;
ZeroInflationCurve::new(
today(),
sample_dates(),
sample_rates(),
Frequency::Monthly,
Actual360::new(),
Linear,
Some(seasonality),
)
}
assert!(built(12).unwrap().has_seasonality());
let deferred = match built(24) {
Ok(_) => panic!("expected the multi-year seasonality to be rejected"),
Err(err) => err,
};
assert!(
deferred.message().contains("#807"),
"{}",
deferred.message()
);
assert!(!sample().has_seasonality());
}
#[test]
fn nodes_round_trip_the_input_dates() {
let curve = sample();
let dates = sample_dates();
let nodes = curve.nodes();
assert_eq!(nodes.len(), dates.len());
for (i, date) in dates.iter().enumerate() {
assert_eq!(*date, nodes[i].0);
}
}
#[test]
fn the_base_node_sits_before_the_reference_date_at_a_negative_time() {
let curve = sample();
assert_eq!(curve.base_date(), base_date());
assert_eq!(curve.reference_date().unwrap(), today());
assert_eq!(curve.times()[0], -57.0 / 360.0);
assert_eq!(curve.times()[1], 7.0 / 360.0);
assert_eq!(
curve.times()[0],
curve.time_from_reference(base_date()).unwrap()
);
}
#[test]
fn zero_rate_date_quantizes_to_the_period_start_and_interpolates() {
let curve = sample();
let mid_march = Date::new(15, Month::March, 2022);
let t = 33.0 / 360.0;
assert_eq!(
t,
curve
.time_from_reference(Date::new(1, Month::March, 2022))
.unwrap()
);
let (t_lo, t_hi) = (curve.times()[3], curve.times()[4]);
let (r_lo, r_hi) = (curve.rates()[3], curve.rates()[4]);
let expected = r_lo + (t - t_lo) / (t_hi - t_lo) * (r_hi - r_lo);
let rate = curve.zero_rate_date(mid_march, false).unwrap();
assert!((rate - expected).abs() < 1.0e-15);
let unquantized = curve
.zero_rate(curve.time_from_reference(mid_march).unwrap(), false)
.unwrap();
assert_ne!(rate, unquantized);
}
#[test]
fn zero_rate_date_reaches_the_base_node_and_stops_below_it() {
let curve = sample();
let rate = curve.zero_rate_date(Date::new(15, Month::December, 2021), false);
assert!(rate.unwrap().is_finite());
let before = curve
.zero_rate_date(Date::new(15, Month::November, 2021), false)
.unwrap_err();
assert!(before.message().contains("is before base date"));
}
#[test]
fn extrapolating_past_the_last_node_errors_where_cpp_extends_the_end_segment() {
let curve = sample();
let beyond = curve.max_date() + Period::new(1, TimeUnit::Years);
let err = curve.zero_rate_date(beyond, true).unwrap_err();
assert!(err.message().contains("extrapolation"));
curve.enable_extrapolation();
assert!(curve.zero_rate_date(beyond, false).is_err());
}
fn build_err(dates: Vec<Date>, rates: Vec<Rate>) -> String {
match build(dates, rates) {
Ok(_) => panic!("expected a construction error"),
Err(err) => err.message().to_string(),
}
}
#[test]
fn constructor_rejects_too_few_dates_before_reading_the_base_date() {
assert!(build_err(vec![], vec![]).contains("too few dates"));
assert!(build_err(vec![base_date()], vec![0.01]).contains("too few dates"));
}
#[test]
fn constructor_rejects_a_count_mismatch_and_unsorted_dates() {
assert!(
build_err(sample_dates(), vec![0.01, 0.01]).contains("indices/dates count mismatch")
);
let mut dates = sample_dates();
dates.swap(1, 2);
assert!(build_err(dates, sample_rates()).contains("dates not sorted"));
}
#[test]
fn the_minus_one_bound_skips_the_base_node() {
let mut rates = sample_rates();
rates[1] = -1.0;
assert!(build_err(sample_dates(), rates).contains("zero inflation data < -100 %"));
let mut rates = sample_rates();
rates[1] = Rate::NAN;
assert!(build_err(sample_dates(), rates).contains("zero inflation data < -100 %"));
let mut rates = sample_rates();
rates[0] = -1.5;
let curve = build(sample_dates(), rates).unwrap();
assert_eq!(curve.rates()[0], -1.5);
}
#[test]
fn the_rate_bound_is_checked_before_the_dates_are_converted_to_times() {
let mut dates = sample_dates();
dates.swap(1, 2);
let mut rates = sample_rates();
rates[1] = -2.0;
assert!(build_err(dates, rates).contains("zero inflation data < -100 %"));
}
#[test]
fn inspectors_expose_the_nodes() {
let curve = sample();
assert_eq!(curve.dates(), &sample_dates()[..]);
assert_eq!(curve.rates(), &sample_rates()[..]);
assert_eq!(curve.data(), curve.rates());
assert_eq!(curve.times().len(), sample_dates().len());
assert_eq!(curve.max_time().unwrap(), *curve.times().last().unwrap());
assert_eq!(curve.frequency(), Frequency::Monthly);
assert_eq!(curve.max_date(), today() + Period::new(10, TimeUnit::Years));
assert!(curve.base_rate().is_err());
}
}