use crate::currency::Currency;
use crate::errors::QlResult;
use crate::handle::Handle;
use crate::indexes::IborIndex;
use crate::indexes::index::Index;
use crate::indexes::interestrateindex::{InterestRateIndex, InterestRateIndexBase};
use crate::instruments::MakeVanillaSwap;
use crate::instruments::VanillaSwap;
use crate::require;
use crate::settings::Settings;
use crate::shared::Shared;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendar::Calendar;
use crate::time::date::Date;
use crate::time::daycounter::DayCounter;
use crate::time::period::Period;
use crate::time::timeunit::TimeUnit;
use crate::types::{Natural, Rate};
pub struct SwapIndex {
base: InterestRateIndexBase,
swap_tenor: Period,
ibor_index: Shared<IborIndex>,
fixed_leg_tenor: Period,
fixed_leg_convention: BusinessDayConvention,
exogenous_discount: bool,
discount: Handle<dyn YieldTermStructure>,
}
impl SwapIndex {
#[allow(clippy::too_many_arguments)]
pub fn new(
family_name: String,
tenor: Period,
settlement_days: Natural,
currency: Currency,
fixing_calendar: Calendar,
fixed_leg_tenor: Period,
fixed_leg_convention: BusinessDayConvention,
fixed_leg_day_counter: DayCounter,
ibor_index: Shared<IborIndex>,
settings: Shared<Settings<Date>>,
) -> SwapIndex {
let base = InterestRateIndexBase::new(
family_name,
tenor,
settlement_days,
currency,
fixing_calendar,
fixed_leg_day_counter,
settings,
);
ibor_index
.base()
.observable()
.register_observer(&base.observer());
SwapIndex {
base,
swap_tenor: tenor,
ibor_index,
fixed_leg_tenor,
fixed_leg_convention,
exogenous_discount: false,
discount: Handle::empty(),
}
}
#[allow(clippy::too_many_arguments)]
pub fn with_exogenous_discount(
family_name: String,
tenor: Period,
settlement_days: Natural,
currency: Currency,
fixing_calendar: Calendar,
fixed_leg_tenor: Period,
fixed_leg_convention: BusinessDayConvention,
fixed_leg_day_counter: DayCounter,
ibor_index: Shared<IborIndex>,
discount: Handle<dyn YieldTermStructure>,
settings: Shared<Settings<Date>>,
) -> SwapIndex {
let base = InterestRateIndexBase::new(
family_name,
tenor,
settlement_days,
currency,
fixing_calendar,
fixed_leg_day_counter,
settings,
);
ibor_index
.base()
.observable()
.register_observer(&base.observer());
discount.register_observer(&base.observer());
SwapIndex {
base,
swap_tenor: tenor,
ibor_index,
fixed_leg_tenor,
fixed_leg_convention,
exogenous_discount: true,
discount,
}
}
pub fn fixed_leg_tenor(&self) -> Period {
self.fixed_leg_tenor
}
pub fn fixed_leg_convention(&self) -> BusinessDayConvention {
self.fixed_leg_convention
}
pub fn ibor_index(&self) -> Shared<IborIndex> {
Shared::clone(&self.ibor_index)
}
pub fn forwarding_term_structure(&self) -> Handle<dyn YieldTermStructure> {
self.ibor_index.forwarding_term_structure().clone()
}
pub fn discounting_term_structure(&self) -> Handle<dyn YieldTermStructure> {
self.discount.clone()
}
pub fn exogenous_discount(&self) -> bool {
self.exogenous_discount
}
pub fn underlying_swap(&self, fixing_date: Date) -> QlResult<VanillaSwap> {
require!(fixing_date != Date::null(), "null fixing date");
let effective = self.value_date(fixing_date)?;
let mut maker = MakeVanillaSwap::new(
self.swap_tenor,
Shared::clone(&self.ibor_index),
Some(0.0),
Period::new(0, TimeUnit::Days),
self.base.settings().clone(),
)
.with_effective_date(effective)
.with_fixed_leg_calendar(self.fixing_calendar())
.with_fixed_leg_day_count(self.day_counter().clone())
.with_fixed_leg_tenor(self.fixed_leg_tenor)
.with_fixed_leg_convention(self.fixed_leg_convention)
.with_fixed_leg_termination_date_convention(self.fixed_leg_convention);
if self.exogenous_discount {
maker = maker.with_discounting_term_structure(self.discount.clone());
}
maker.build()
}
}
impl InterestRateIndex for SwapIndex {
fn base(&self) -> &InterestRateIndexBase {
&self.base
}
fn maturity_date(&self, value_date: Date) -> QlResult<Date> {
let fix_date = self.fixing_date(value_date);
self.underlying_swap(fix_date)?
.fixed_vs_floating()
.maturity_date()
}
fn forecast_fixing(&self, fixing_date: Date) -> QlResult<Rate> {
let mut swap = self.underlying_swap(fixing_date)?;
swap.fixed_vs_floating_mut().fair_rate()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::indexes::ibor::Euribor;
use crate::interestrate::Compounding;
use crate::shared::shared;
use crate::termstructures::yields::FlatForward;
use crate::time::calendars::target::Target;
use crate::time::date::Month;
use crate::time::daycounters::actual360::Actual360;
use crate::time::daycounters::thirty360::{Convention, Thirty360};
use crate::time::frequency::Frequency;
fn today() -> Date {
Date::new(9, Month::October, 2015)
}
fn settings_today() -> Shared<Settings<Date>> {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(today());
settings
}
fn flat_curve(rate: Rate) -> Handle<dyn YieldTermStructure> {
Handle::new(shared(FlatForward::with_rate(
today(),
rate,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>)
}
fn euribor_swap_5y(
curve: Handle<dyn YieldTermStructure>,
settings: &Shared<Settings<Date>>,
) -> (SwapIndex, Shared<IborIndex>) {
let euribor6m = shared(Euribor::six_months(curve, Shared::clone(settings)));
let index = SwapIndex::new(
"EuriborSwapIsdaFixA".into(),
Period::new(5, TimeUnit::Years),
2,
Currency::eur(),
Target::new(),
Period::new(1, TimeUnit::Years),
BusinessDayConvention::ModifiedFollowing,
Thirty360::with_convention(Convention::BondBasis),
Shared::clone(&euribor6m),
Shared::clone(settings),
);
(index, euribor6m)
}
#[test]
fn forecast_fixing_is_the_underlying_swap_fair_rate() {
let settings = settings_today();
let curve = flat_curve(0.05);
let (index, euribor6m) = euribor_swap_5y(curve, &settings);
let fixing_date = Date::new(15, Month::October, 2015);
let got = index.forecast_fixing(fixing_date).unwrap();
let effective = index.value_date(fixing_date).unwrap();
let mut reference = MakeVanillaSwap::new(
Period::new(5, TimeUnit::Years),
euribor6m,
Some(0.0),
Period::new(0, TimeUnit::Days),
Shared::clone(&settings),
)
.with_effective_date(effective)
.with_fixed_leg_calendar(Target::new())
.with_fixed_leg_day_count(Thirty360::with_convention(Convention::BondBasis))
.with_fixed_leg_tenor(Period::new(1, TimeUnit::Years))
.with_fixed_leg_convention(BusinessDayConvention::ModifiedFollowing)
.with_fixed_leg_termination_date_convention(BusinessDayConvention::ModifiedFollowing)
.build()
.unwrap();
let expected = reference.fixed_vs_floating_mut().fair_rate().unwrap();
assert!(
(got - expected).abs() < 1e-14,
"forecast fixing {got} vs underlying swap fair rate {expected}"
);
assert!(got > 0.0, "a positive swap rate off a 5% curve");
}
#[test]
fn maturity_date_is_the_underlying_swap_maturity() {
let settings = settings_today();
let curve = flat_curve(0.05);
let (index, _euribor6m) = euribor_swap_5y(curve, &settings);
let value_date = Date::new(15, Month::October, 2015);
let fix_date = index.fixing_date(value_date);
let expected = index
.underlying_swap(fix_date)
.unwrap()
.fixed_vs_floating()
.maturity_date()
.unwrap();
assert_eq!(index.maturity_date(value_date).unwrap(), expected);
assert!(
expected > value_date + Period::new(4, TimeUnit::Years)
&& expected < value_date + Period::new(6, TimeUnit::Years),
"a 5Y swap matures about five years out, got {expected:?}"
);
}
#[test]
fn null_fixing_date_is_rejected() {
let settings = settings_today();
let curve = flat_curve(0.05);
let (index, _euribor6m) = euribor_swap_5y(curve, &settings);
assert!(index.underlying_swap(Date::null()).is_err());
}
#[test]
fn inspectors_report_the_conventions() {
let settings = settings_today();
let curve = flat_curve(0.05);
let (index, _euribor6m) = euribor_swap_5y(curve, &settings);
assert_eq!(index.fixed_leg_tenor(), Period::new(1, TimeUnit::Years));
assert_eq!(
index.fixed_leg_convention(),
BusinessDayConvention::ModifiedFollowing
);
assert!(!index.exogenous_discount());
assert!(index.discounting_term_structure().is_empty());
assert!(!index.forwarding_term_structure().is_empty());
}
}