use super::cpibasedate::CpiBaseDateCashFlow;
use super::cpicoupon::{check_base_cpi, checked_cpi_offset, checked_cpi_shift, finite_cpi};
use super::cpidates::{checked_cpi_adjust, checked_cpi_day};
use super::{CpiCashFlow, CpiCoupon, CpiCouponPricer, FixedRateCoupon};
use crate::cashflow::{CashFlow, Leg};
use crate::errors::QlResult;
use crate::indexes::inflationindex::{CpiInterpolationType, ZeroInflationIndex};
use crate::require;
use crate::shared::{Shared, shared};
use crate::time::{
businessdayconvention::BusinessDayConvention,
calendar::Calendar,
date::Date,
daycounter::DayCounter,
daycounters::thirty360::{Convention, Thirty360},
period::Period,
schedule::Schedule,
};
#[derive(Clone, Copy)]
enum CpiLegBase {
Explicit(f64),
Date(Option<Date>),
}
#[derive(Clone)]
#[must_use]
pub struct CpiLeg {
schedule: Schedule,
index: Shared<ZeroInflationIndex>,
base: CpiLegBase,
lag: Period,
interpolation: CpiInterpolationType,
notionals: Vec<f64>,
rates: Vec<f64>,
day_counter: DayCounter,
payment_calendar: Calendar,
adjustment: BusinessDayConvention,
growth_only: bool,
ex_coupon: Option<(Period, Calendar, BusinessDayConvention, bool)>,
}
impl CpiLeg {
pub fn new(
schedule: Schedule,
index: Shared<ZeroInflationIndex>,
base_cpi: f64,
observation_lag: Period,
) -> Self {
let payment_calendar = schedule.calendar().clone();
Self {
schedule,
index,
base: CpiLegBase::Explicit(base_cpi),
lag: observation_lag,
interpolation: CpiInterpolationType::Flat,
notionals: Vec::new(),
rates: Vec::new(),
day_counter: Thirty360::with_convention(Convention::BondBasis),
payment_calendar,
adjustment: BusinessDayConvention::ModifiedFollowing,
growth_only: true,
ex_coupon: None,
}
}
pub fn from_base_date(
schedule: Schedule,
index: Shared<ZeroInflationIndex>,
base_date: Option<Date>,
observation_lag: Period,
) -> Self {
let mut leg = Self::new(schedule, index, 1.0, observation_lag);
leg.base = CpiLegBase::Date(base_date);
leg
}
pub fn with_notional(self, value: f64) -> Self {
self.with_notionals(vec![value])
}
pub fn with_notionals(mut self, values: Vec<f64>) -> Self {
self.notionals = values;
self
}
pub fn with_fixed_rate(self, value: f64) -> Self {
self.with_fixed_rates(vec![value])
}
pub fn with_fixed_rates(mut self, values: Vec<f64>) -> Self {
self.rates = values;
self
}
pub fn with_payment_day_counter(mut self, value: DayCounter) -> Self {
self.day_counter = value;
self
}
pub fn with_payment_adjustment(mut self, value: BusinessDayConvention) -> Self {
self.adjustment = value;
self
}
pub fn with_payment_calendar(mut self, value: Calendar) -> Self {
self.payment_calendar = value;
self
}
pub fn with_observation_interpolation(mut self, value: CpiInterpolationType) -> Self {
self.interpolation = value;
self
}
pub fn with_subtract_inflation_nominal(mut self, value: bool) -> Self {
self.growth_only = value;
self
}
pub fn with_ex_coupon_period(
mut self,
period: Period,
calendar: Calendar,
convention: BusinessDayConvention,
end_of_month: bool,
) -> Self {
self.ex_coupon = if period.length() == 0 {
None
} else {
Some((period, calendar, convention, end_of_month))
};
self
}
pub fn build(&self) -> QlResult<Leg> {
require!(!self.schedule.is_empty(), "empty CPI schedule");
let base_date = match self.base {
CpiLegBase::Explicit(base) => {
check_base_cpi(base)?;
None
}
CpiLegBase::Date(date) => Some(match date {
Some(date) => date,
None => {
require!(
self.schedule.len() > 1,
"base date required for a single-date CPI leg"
);
checked_cpi_shift(self.schedule.date(0), self.lag)?
}
}),
};
require!(!self.notionals.is_empty(), "no CPI notional given");
let periods = self.schedule.len() - 1;
require!(
periods == 0 || !self.rates.is_empty(),
"no CPI fixed rates given"
);
require!(
self.notionals.len() <= periods + 1 && self.rates.len() <= periods,
"too many CPI leg parameters"
);
for v in self.notionals.iter().chain(self.rates.iter()) {
finite_cpi(*v)?;
}
for date in self.schedule.dates() {
checked_cpi_shift(*date, self.lag)?;
}
let mut flows = Vec::with_capacity(periods + 1);
let last = |v: &[f64], i: usize| v[i.min(v.len() - 1)];
let pricer = shared(CpiCouponPricer::default());
for i in 0..periods {
let start = self.schedule.date(i);
let end = self.schedule.date(i + 1);
require!(start < end, "CPI schedule dates must increase");
let mut ref_start = start;
let mut ref_end = end;
if self.schedule.has_is_regular()
&& self.schedule.has_tenor()
&& !self.schedule.is_regular_at(i + 1)
{
if i == 0 {
ref_start = checked_cpi_adjust(
self.schedule.calendar(),
checked_cpi_shift(end, self.schedule.tenor())?,
self.schedule.business_day_convention(),
)?;
}
if i + 1 == periods {
let tenor = self.schedule.tenor();
ref_end = checked_cpi_adjust(
self.schedule.calendar(),
checked_cpi_offset(start, tenor, true)?,
self.schedule.business_day_convention(),
)?;
}
}
let payment = checked_cpi_adjust(&self.payment_calendar, end, self.adjustment)?;
let ex_coupon = if let Some((period, cal, conv, eom)) = &self.ex_coupon {
checked_cpi_shift(payment, *period)?;
Some(ex_coupon_date(cal, payment, *period, *conv, *eom)?)
} else {
None
};
let nominal = last(&self.notionals, i);
let rate = last(&self.rates, i);
let flow: Shared<dyn CashFlow> = if rate == 0.0 {
shared(FixedRateCoupon::from_rate(
payment,
nominal,
rate,
self.day_counter.clone(),
start,
end,
Some(ref_start),
Some(ref_end),
ex_coupon,
))
} else {
let constructor = |base| {
CpiCoupon::new(
base,
payment,
nominal,
start,
end,
self.index.clone(),
self.lag,
self.interpolation,
self.day_counter.clone(),
rate,
Some(ref_start),
Some(ref_end),
ex_coupon,
)
};
let coupon = match self.base {
CpiLegBase::Explicit(base) => constructor(base)?,
CpiLegBase::Date(_) => CpiCoupon::from_base_date(
base_date.ok_or_else(|| {
crate::errors::QlError::new("missing CPI base date", file!(), line!())
})?,
payment,
nominal,
start,
end,
self.index.clone(),
self.lag,
self.interpolation,
self.day_counter.clone(),
rate,
Some(ref_start),
Some(ref_end),
ex_coupon,
)?,
};
coupon.set_pricer(pricer.clone());
shared(coupon)
};
flows.push(flow);
}
let observation = self.schedule.date(periods);
let payment = checked_cpi_adjust(&self.payment_calendar, observation, self.adjustment)?;
let principal: Shared<dyn CashFlow> = match self.base {
CpiLegBase::Explicit(base) => shared(CpiCashFlow::new(
last(&self.notionals, periods),
self.index.clone(),
base,
observation,
self.lag,
self.interpolation,
payment,
self.growth_only,
)?),
CpiLegBase::Date(_) => shared(CpiBaseDateCashFlow::new(
last(&self.notionals, periods),
self.index.clone(),
base_date.ok_or_else(|| {
crate::errors::QlError::new("missing CPI base date", file!(), line!())
})?,
observation,
self.lag,
self.interpolation,
payment,
self.growth_only,
)?),
};
flows.push(principal);
Ok(flows)
}
}
fn ex_coupon_date(
calendar: &Calendar,
payment: crate::time::date::Date,
period: Period,
convention: BusinessDayConvention,
eom: bool,
) -> QlResult<crate::time::date::Date> {
use crate::time::{date::Date, timeunit::TimeUnit};
checked_cpi_shift(payment, period)?;
if period.units() == TimeUnit::Days && period.length() != 0 {
let mut date = payment;
for _ in 0..period.length() {
date = checked_cpi_day(date, -1)?;
while calendar.is_holiday(date) {
date = checked_cpi_day(date, -1)?;
}
}
return Ok(date);
}
let shifted = checked_cpi_shift(payment, period)?;
if eom && matches!(period.units(), TimeUnit::Months | TimeUnit::Years) {
let end = if convention == BusinessDayConvention::Unadjusted {
Date::end_of_month(payment)
} else {
checked_cpi_adjust(
calendar,
Date::end_of_month(payment),
BusinessDayConvention::Preceding,
)?
};
if payment >= end {
return if convention == BusinessDayConvention::Unadjusted {
Ok(Date::end_of_month(shifted))
} else {
checked_cpi_adjust(
calendar,
Date::end_of_month(shifted),
BusinessDayConvention::Preceding,
)
};
}
}
checked_cpi_adjust(calendar, shifted, convention)
}