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.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.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);
}
if (self.frequency as i16 as usize) == self.seasonality_factors.len() {
return Ok(true);
}
fail!(
"multi-year seasonality consistency check is not ported (#807): \
{} factors at {} frequency",
self.seasonality_factors.len(),
self.frequency
)
}
}
#[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_covers_the_stationary_case_and_defers_the_multi_year_one() {
let curve = a_curve();
assert!(monthly(12).is_consistent(&curve).unwrap());
let deferred = monthly(24).is_consistent(&curve).unwrap_err();
assert!(deferred.message().contains("#807"));
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"));
}
}