use crate::errors::QlResult;
use crate::indexes::inflationindex::inflation_period;
use crate::termstructures::inflation::inflationtermstructure::InflationTermStructure;
use crate::time::date::Date;
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};
use crate::{fail, require};
pub trait Seasonality {
fn correct_zero_rate(
&self,
date: Date,
rate: Rate,
its: &dyn InflationTermStructure,
) -> QlResult<Rate>;
fn correct_yoy_rate(
&self,
date: Date,
rate: Rate,
its: &dyn InflationTermStructure,
) -> QlResult<Rate>;
fn is_consistent(&self, _its: &dyn InflationTermStructure) -> QlResult<bool> {
Ok(true)
}
}
#[derive(Debug)]
pub struct MultiplicativePriceSeasonality {
seasonality_base_date: Date,
frequency: Frequency,
seasonality_factors: Vec<Rate>,
}
impl MultiplicativePriceSeasonality {
pub fn new(
seasonality_base_date: Date,
frequency: Frequency,
seasonality_factors: Vec<Rate>,
) -> QlResult<MultiplicativePriceSeasonality> {
let seasonality = MultiplicativePriceSeasonality {
seasonality_base_date,
frequency,
seasonality_factors,
};
seasonality.validate()?;
Ok(seasonality)
}
pub fn set(
&mut self,
seasonality_base_date: Date,
frequency: Frequency,
seasonality_factors: Vec<Rate>,
) -> QlResult<()> {
*self = MultiplicativePriceSeasonality::new(
seasonality_base_date,
frequency,
seasonality_factors,
)?;
Ok(())
}
pub fn seasonality_base_date(&self) -> Date {
self.seasonality_base_date
}
pub fn frequency(&self) -> Frequency {
self.frequency
}
pub fn seasonality_factors(&self) -> &[Rate] {
&self.seasonality_factors
}
pub fn seasonality_factor(&self, to: Date) -> QlResult<Rate> {
let from = self.seasonality_base_date;
let factor_frequency = self.frequency;
let n_factors = self.seasonality_factors.len();
let factor_period = Period::try_from(factor_frequency)?;
let which = if from == to {
0
} else {
let diff_days = (to - from).abs();
let dir: Integer = if from > to { -1 } else { 1 };
let diff = match factor_period.units() {
TimeUnit::Days => dir * diff_days,
TimeUnit::Weeks => dir * (diff_days / 7),
TimeUnit::Months => {
let lim = inflation_period(to, factor_frequency)?;
let mut steps = diff_days / (31 * factor_period.length());
let mut go = from + factor_period * (dir * steps);
while !(lim.0 <= go && go <= lim.1) {
go = go + factor_period * dir;
steps += 1;
}
dir * steps
}
TimeUnit::Years => fail!(
"seasonality period time unit is not allowed to be : {}",
factor_period.units()
),
unit => fail!("Unknown time unit: {unit}"),
};
if dir == 1 {
(diff as usize) % n_factors
} else {
(n_factors - ((-diff) as usize) % n_factors) % n_factors
}
};
Ok(self.seasonality_factors[which])
}
fn validate(&self) -> QlResult<()> {
match self.frequency {
Frequency::Semiannual
| Frequency::EveryFourthMonth
| Frequency::Quarterly
| Frequency::Bimonthly
| Frequency::Monthly
| Frequency::Biweekly
| Frequency::Weekly
| Frequency::Daily => {
require!(
!self.seasonality_factors.is_empty(),
"no seasonality factors given"
);
let per_year = self.frequency as i16;
require!(
self.seasonality_factors
.len()
.is_multiple_of(per_year as usize),
"For frequency {} require multiple of {per_year} factors {} were given.",
self.frequency,
self.seasonality_factors.len()
);
Ok(())
}
frequency => fail!(
"bad frequency specified: {frequency}, \
only semi-annual through daily permitted."
),
}
}
fn seasonality_correction(
&self,
rate: Rate,
at_date: Date,
day_counter: &DayCounter,
curve_base_date: Date,
is_zero_rate: bool,
) -> QlResult<Rate> {
let factor_at = self.seasonality_factor(at_date)?;
let f = if is_zero_rate {
let factor_base = self.seasonality_factor(curve_base_date)?;
let seasonality_at = factor_at / factor_base;
let (period_start, _) = inflation_period(at_date, self.frequency)?;
let time_from_curve_base = day_counter.year_fraction(curve_base_date, period_start);
seasonality_at.powf(1.0 / time_from_curve_base)
} else {
let a_year_before = at_date - Period::new(1, TimeUnit::Years);
factor_at / self.seasonality_factor(a_year_before)?
};
Ok((rate + 1.0) * f - 1.0)
}
}
impl Seasonality for MultiplicativePriceSeasonality {
fn correct_zero_rate(
&self,
date: Date,
rate: Rate,
its: &dyn InflationTermStructure,
) -> QlResult<Rate> {
let curve_base_date = its.try_base_date()?;
let (effective_fixing_date, _) = inflation_period(date, its.frequency())?;
self.seasonality_correction(
rate,
effective_fixing_date,
&its.require_day_counter()?,
curve_base_date,
true,
)
}
fn correct_yoy_rate(
&self,
date: Date,
rate: Rate,
its: &dyn InflationTermStructure,
) -> QlResult<Rate> {
let (_, curve_base_date) = inflation_period(its.try_base_date()?, its.frequency())?;
self.seasonality_correction(
rate,
date,
&its.require_day_counter()?,
curve_base_date,
false,
)
}
fn is_consistent(&self, its: &dyn InflationTermStructure) -> QlResult<bool> {
if self.frequency == Frequency::Daily {
return Ok(true);
}
let frequency = self.frequency as i16 as usize;
if frequency == self.seasonality_factors.len() {
return Ok(true);
}
let (_, curve_base_date) = inflation_period(its.try_base_date()?, its.frequency())?;
let factor_base = self.seasonality_factor(curve_base_date)?;
let available_years = (Date::max_date().year() - curve_base_date.year()) as usize;
for year in 1..self.seasonality_factors.len() / frequency {
require!(
year <= available_years,
"seasonality consistency date exceeds the supported date range: {} years after {}",
year,
curve_base_date
);
let factor_at = self.seasonality_factor(
curve_base_date + Period::new(year as Integer, TimeUnit::Years),
)?;
let consistent = (factor_at - factor_base).abs() < 1.0e-5;
require!(
consistent,
"seasonality is inconsistent with inflation term structure, factors {} and later factor {}, {} years later from inflation curve with base date at {}",
factor_base,
factor_at,
year,
curve_base_date
);
}
Ok(true)
}
}
#[derive(Debug)]
pub struct KerkhofSeasonality {
seasonality_base_date: Date,
seasonality_factors: Vec<Rate>,
}
impl KerkhofSeasonality {
pub fn new(seasonality_base_date: Date, seasonality_factors: Vec<Rate>) -> QlResult<Self> {
require!(
seasonality_factors.len() == 12,
"12 monthly seasonal factors needed for Kerkhof Seasonality: got {}",
seasonality_factors.len()
);
Ok(Self {
seasonality_base_date,
seasonality_factors,
})
}
pub fn set(
&mut self,
seasonality_base_date: Date,
seasonality_factors: Vec<Rate>,
) -> QlResult<()> {
*self = Self::new(seasonality_base_date, seasonality_factors)?;
Ok(())
}
pub fn seasonality_base_date(&self) -> Date {
self.seasonality_base_date
}
pub fn frequency(&self) -> Frequency {
Frequency::Monthly
}
pub fn seasonality_factors(&self) -> &[Rate] {
&self.seasonality_factors
}
pub fn seasonality_factor(&self, to: Date) -> QlResult<Rate> {
let from_month = self.seasonality_base_date.month() as usize;
let to_month = to.month() as usize;
let factor = self.seasonality_factors[from_month.min(to_month)..from_month.max(to_month)]
.iter()
.fold(1.0, |product, value| product * value);
Ok(if to_month < from_month {
1.0 / factor
} else {
factor
})
}
}
impl Seasonality for KerkhofSeasonality {
fn correct_zero_rate(
&self,
date: Date,
rate: Rate,
its: &dyn InflationTermStructure,
) -> QlResult<Rate> {
let (effective_date, _) = inflation_period(date, its.frequency())?;
let (base_month, _) = inflation_period(its.try_base_date()?, Frequency::Monthly)?;
let time = its
.require_day_counter()?
.year_fraction(base_month, effective_date);
Ok((rate + 1.0) * self.seasonality_factor(effective_date)?.powf(1.0 / time) - 1.0)
}
fn correct_yoy_rate(
&self,
_date: Date,
_rate: Rate,
_its: &dyn InflationTermStructure,
) -> QlResult<Rate> {
fail!("Seasonal Kerkhof model is not defined on YoY rates")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::interpolations::linear::Linear;
use crate::termstructures::inflation::interpolatedzeroinflationcurve::ZeroInflationCurve;
use crate::time::date::Month::{August, December, January, July};
use crate::time::daycounters::thirty360::{Convention, Thirty360};
fn factors(count: usize) -> Vec<Rate> {
(0..count).map(|i| 1.0 + i as Rate / 1000.0).collect()
}
fn seasonality_base_date() -> Date {
Date::new(31, January, 2007)
}
fn curve_base_date() -> Date {
Date::new(1, July, 2007)
}
fn monthly(count: usize) -> MultiplicativePriceSeasonality {
MultiplicativePriceSeasonality::new(
seasonality_base_date(),
Frequency::Monthly,
factors(count),
)
.expect("a whole multiple of twelve factors")
}
fn a_curve() -> ZeroInflationCurve {
ZeroInflationCurve::new(
Date::new(13, August, 2007),
vec![curve_base_date(), Date::new(13, August, 2012)],
vec![0.02, 0.03],
Frequency::Monthly,
Thirty360::with_convention(Convention::BondBasis),
Linear,
None,
)
.expect("two sorted nodes and plausible rates")
}
#[test]
fn the_factor_is_picked_by_stepped_month_offset_and_wraps_both_ways() {
let seasonality = monthly(12);
assert_eq!(
seasonality
.seasonality_factor(seasonality_base_date())
.unwrap(),
factors(12)[0]
);
assert_eq!(
seasonality.seasonality_factor(curve_base_date()).unwrap(),
factors(12)[6]
);
assert_eq!(
seasonality
.seasonality_factor(Date::new(1, August, 2008))
.unwrap(),
factors(12)[7]
);
assert_eq!(
seasonality
.seasonality_factor(Date::new(1, December, 2006))
.unwrap(),
factors(12)[11]
);
assert_eq!(
seasonality
.seasonality_factor(Date::new(1, December, 2005))
.unwrap(),
factors(12)[11]
);
}
#[test]
fn the_correction_is_the_rate_grossed_up_by_the_factor_ratio() {
let seasonality = monthly(24);
let day_counter = Thirty360::with_convention(Convention::BondBasis);
let at = Date::new(1, August, 2008);
let rate = 0.03;
let factor_at = seasonality.seasonality_factor(at).unwrap();
let a_year_before = seasonality
.seasonality_factor(Date::new(1, August, 2007))
.unwrap();
assert_ne!(factor_at, a_year_before, "a two-year factor set moves YoY");
assert_eq!(
seasonality
.seasonality_correction(rate, at, &day_counter, Date::null(), false)
.unwrap(),
(rate + 1.0) * (factor_at / a_year_before) - 1.0
);
let factor_base = seasonality.seasonality_factor(curve_base_date()).unwrap();
let f = (factor_at / factor_base).powf(1.0 / (390.0 / 360.0));
assert_eq!(
seasonality
.seasonality_correction(rate, at, &day_counter, curve_base_date(), true)
.unwrap(),
(rate + 1.0) * f - 1.0
);
}
#[test]
fn the_zero_correction_normalizes_against_the_curves_true_base_date() {
let seasonality = monthly(12);
let curve = a_curve();
let rate = 0.03;
let expected_f = (factors(12)[7] / factors(12)[6]).powf(1.0 / (390.0 / 360.0));
assert_eq!(
seasonality
.correct_zero_rate(Date::new(15, August, 2008), rate, &curve)
.unwrap(),
(rate + 1.0) * expected_f - 1.0
);
assert!(expected_f > 1.0, "the factor set rises over that span");
assert_ne!(
seasonality
.correct_zero_rate(Date::new(15, August, 2008), rate, &curve)
.unwrap(),
rate,
"the correction must move the rate at all"
);
}
#[test]
fn a_stationary_factor_set_does_not_move_a_year_on_year_rate() {
let seasonality = monthly(12);
let curve = a_curve();
let at = Date::new(1, August, 2008);
assert_eq!(
seasonality.seasonality_factor(at).unwrap(),
seasonality
.seasonality_factor(Date::new(1, August, 2007))
.unwrap()
);
let corrected = seasonality.correct_yoy_rate(at, 0.03, &curve).unwrap();
assert!((corrected - 0.03).abs() < 1.0e-15, "moved to {corrected}");
}
#[test]
fn consistency_accepts_stationary_and_rejects_inconsistent_multi_year_factors() {
let curve = a_curve();
assert!(monthly(12).is_consistent(&curve).unwrap());
let inconsistent = monthly(24).is_consistent(&curve).unwrap_err();
assert!(
inconsistent
.message()
.contains("seasonality is inconsistent")
);
let rejected = MultiplicativePriceSeasonality::new(
seasonality_base_date(),
Frequency::Monthly,
factors(13),
)
.unwrap_err();
assert!(
rejected
.message()
.contains("require multiple of 12 factors 13 were given")
);
}
#[test]
fn only_semiannual_through_daily_frequencies_are_accepted() {
let annual = MultiplicativePriceSeasonality::new(
seasonality_base_date(),
Frequency::Annual,
factors(1),
)
.unwrap_err();
assert!(annual.message().contains("bad frequency specified"));
let empty = MultiplicativePriceSeasonality::new(
seasonality_base_date(),
Frequency::Monthly,
vec![],
)
.unwrap_err();
assert!(empty.message().contains("no seasonality factors given"));
}
}