use super::super::bond::Bond;
use crate::cashflows::FixedRateLeg;
use crate::errors::QlResult;
use crate::interestrate::Compounding;
use crate::require;
use crate::settings::Settings;
use crate::shared::Shared;
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendar::Calendar;
use crate::time::date::Date;
use crate::time::daycounter::DayCounter;
use crate::time::frequency::Frequency;
use crate::time::period::Period;
use crate::time::schedule::Schedule;
use crate::types::{Natural, Rate, Real};
pub struct FixedRateBond {
bond: Bond,
frequency: Frequency,
day_counter: DayCounter,
first_period_day_counter: Option<DayCounter>,
}
impl FixedRateBond {
#[allow(clippy::too_many_arguments)]
pub fn new(
settlement_days: Natural,
face_amount: Real,
schedule: Schedule,
coupons: Vec<Rate>,
accrual_day_counter: DayCounter,
payment_convention: BusinessDayConvention,
redemption: Real,
issue_date: Option<Date>,
payment_calendar: Option<Calendar>,
ex_coupon_period: Option<Period>,
ex_coupon_calendar: Calendar,
ex_coupon_convention: BusinessDayConvention,
ex_coupon_end_of_month: bool,
first_period_day_counter: Option<DayCounter>,
settings: Shared<Settings<Date>>,
) -> QlResult<FixedRateBond> {
let calendar = payment_calendar.unwrap_or_else(|| schedule.calendar().clone());
let maturity = schedule.end_date();
let frequency = if schedule.has_tenor() {
schedule.tenor().frequency()
} else {
Frequency::NoFrequency
};
let mut leg = FixedRateLeg::new(schedule)
.with_notional(face_amount)
.with_coupon_rates(
coupons,
accrual_day_counter.clone(),
Compounding::Simple,
Frequency::Annual,
)?
.with_payment_calendar(calendar.clone())
.with_payment_adjustment(payment_convention);
if let Some(day_counter) = &first_period_day_counter {
leg = leg.with_first_period_day_counter(day_counter.clone());
}
if let Some(period) = ex_coupon_period {
leg = leg.with_ex_coupon_period(
period,
ex_coupon_calendar,
ex_coupon_convention,
ex_coupon_end_of_month,
);
}
let cashflows = leg.build()?;
let mut bond = Bond::new(settlement_days, calendar, issue_date, cashflows, settings)?;
bond.add_redemptions_to_cashflows(&[redemption])?;
bond.set_maturity_date(maturity);
require!(!bond.cashflows().is_empty(), "bond with no cashflows!");
require!(
bond.redemptions().len() == 1,
"multiple redemptions created"
);
Ok(FixedRateBond {
bond,
frequency,
day_counter: accrual_day_counter,
first_period_day_counter,
})
}
pub fn frequency(&self) -> Frequency {
self.frequency
}
pub fn day_counter(&self) -> &DayCounter {
&self.day_counter
}
pub fn first_period_day_counter(&self) -> Option<&DayCounter> {
self.first_period_day_counter.as_ref()
}
pub fn bond(&self) -> &Bond {
&self.bond
}
pub fn bond_mut(&mut self) -> &mut Bond {
&mut self.bond
}
pub fn into_bond(self) -> Bond {
self.bond
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shared::shared;
use crate::time::calendars::nullcalendar::NullCalendar;
use crate::time::calendars::unitedstates::{Market, UnitedStates};
use crate::time::date::Month;
use crate::time::daycounters::actual360::Actual360;
use crate::time::daycounters::actualactual::{ActualActual, Convention};
use crate::time::schedule::MakeSchedule;
fn settings_today() -> Shared<Settings<Date>> {
let settings = shared(Settings::new());
settings.set_evaluation_date(Date::new(30, Month::November, 2004));
settings
}
fn annual_schedule() -> Schedule {
MakeSchedule::new()
.from(Date::new(30, Month::November, 2004))
.to(Date::new(30, Month::November, 2006))
.with_frequency(Frequency::Annual)
.with_convention(BusinessDayConvention::Unadjusted)
.backwards()
.build()
}
fn plain_bond() -> FixedRateBond {
FixedRateBond::new(
3,
100.0,
annual_schedule(),
vec![0.025],
Actual360::new(),
BusinessDayConvention::ModifiedFollowing,
100.0,
Some(Date::new(30, Month::November, 2004)),
None,
None,
NullCalendar::new(),
BusinessDayConvention::Unadjusted,
false,
None,
settings_today(),
)
.unwrap()
}
#[test]
fn it_builds_two_coupons_and_a_full_redemption() {
let bond = plain_bond();
assert_eq!(
bond.bond().cashflows().len(),
3,
"two annual coupons plus the redemption"
);
assert_eq!(bond.bond().redemptions().len(), 1);
assert_eq!(bond.bond().redemptions()[0].amount().unwrap(), 100.0);
assert_eq!(
bond.bond().redemptions()[0].date(),
Date::new(30, Month::November, 2006)
);
assert_eq!(bond.bond().notionals(), &[100.0, 0.0]);
assert_eq!(bond.frequency(), Frequency::Annual);
assert_eq!(bond.day_counter().name(), Actual360::new().name());
assert!(bond.first_period_day_counter().is_none());
assert_eq!(
bond.bond().issue_date(),
Some(Date::new(30, Month::November, 2004))
);
}
#[test]
fn a_below_par_redemption_scales_the_amount() {
let bond = FixedRateBond::new(
3,
100.0,
annual_schedule(),
vec![0.025],
Actual360::new(),
BusinessDayConvention::ModifiedFollowing,
98.0,
None,
None,
None,
NullCalendar::new(),
BusinessDayConvention::Unadjusted,
false,
None,
settings_today(),
)
.unwrap();
assert_eq!(bond.bond().redemptions().len(), 1);
assert_eq!(bond.bond().redemptions()[0].amount().unwrap(), 98.0);
}
#[test]
fn the_maturity_is_the_schedule_end_not_the_last_payment() {
let schedule = MakeSchedule::new()
.from(Date::new(30, Month::November, 2004))
.to(Date::new(30, Month::November, 2008))
.with_frequency(Frequency::Semiannual)
.with_calendar(UnitedStates::new(Market::GovernmentBond))
.with_convention(BusinessDayConvention::Unadjusted)
.with_termination_date_convention(BusinessDayConvention::Unadjusted)
.backwards()
.build();
let bond = FixedRateBond::new(
1,
100.0,
schedule,
vec![0.02875],
ActualActual::with_convention(Convention::ISMA),
BusinessDayConvention::ModifiedFollowing,
100.0,
Some(Date::new(30, Month::November, 2004)),
None,
None,
NullCalendar::new(),
BusinessDayConvention::Unadjusted,
false,
None,
settings_today(),
)
.unwrap();
let maturity = bond.bond().maturity_date().unwrap();
let last_payment = bond.bond().redemptions()[0].date();
assert_eq!(maturity, Date::new(30, Month::November, 2008));
assert_eq!(last_payment, Date::new(28, Month::November, 2008));
assert_ne!(
maturity, last_payment,
"the explicit schedule-end maturity wins over the derived last-payment date"
);
}
}