use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use crate::derivatives::basic::Direction;
use crate::error::Result;
use crate::markets::interestrate::interestrateindex::InterestRateIndex;
use crate::markets::termstructures::yieldcurve::{
InterestRateQuote, InterestRateQuoteEnum, InterpolationMethodEnum, StrippedCurve,
YieldTermStructure,
};
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendars::{Calendar, Target};
use crate::time::daycounters::DayCounters;
use crate::time::daycounters::actual365fixed::Actual365Fixed;
use crate::time::frequency::Frequency;
use crate::time::period::Period;
use roots::{SimpleConvergency, find_root_brent};
#[derive(Deserialize, Serialize, Debug)]
pub enum InterestRateSwapLegType {
Float { spread: f64 },
Fixed { coupon: f64 },
}
#[derive(Deserialize, Serialize, Debug)]
pub struct ScheduleDetail {
pub frequency: Frequency,
pub tenor: Period,
pub duration: Period,
pub day_counter: Box<dyn DayCounters>,
pub calendar: Box<dyn Calendar>,
pub convention: BusinessDayConvention,
pub settlement_days: i64,
pub pay_delay: i64,
pub days_before_accrual: i64,
}
impl ScheduleDetail {
#[allow(clippy::too_many_arguments)]
pub fn new(
frequency: Frequency,
tenor: Period,
duration: Period,
day_counter: Box<dyn DayCounters>,
calendar: Box<dyn Calendar>,
convention: BusinessDayConvention,
settlement_days: i64,
pay_delay: i64,
days_before_accrual: i64,
) -> Self {
Self {
frequency,
tenor,
duration,
day_counter,
calendar,
convention,
settlement_days,
pay_delay,
days_before_accrual,
}
}
}
#[derive(Deserialize, Serialize, Debug)]
pub struct InterestRateSwapLeg {
pub swap_type: InterestRateSwapLegType,
pub direction: Direction,
pub interest_rate_index: InterestRateIndex,
pub notional: f64,
pub schedule_detail: ScheduleDetail,
pub schedule: Vec<InterestRateSchedulePeriod>,
}
impl InterestRateSwapLeg {
pub fn new(
swap_type: InterestRateSwapLegType,
direction: Direction,
interest_rate_index: InterestRateIndex,
notional: f64,
schedule_detail: ScheduleDetail,
schedule: Vec<InterestRateSchedulePeriod>,
) -> Self {
Self {
swap_type,
direction,
interest_rate_index,
notional,
schedule_detail,
schedule,
}
}
pub fn effective_date(&self, valuation_date: NaiveDate) -> Result<Option<NaiveDate>> {
self.schedule_detail.calendar.advance(
valuation_date,
Period::Days(self.interest_rate_index.settlement_days),
self.schedule_detail.convention,
Some(self.interest_rate_index.end_of_month),
)
}
pub fn is_fixe_leg(&self) -> bool {
match self.swap_type {
InterestRateSwapLegType::Fixed { coupon: _ } => true,
InterestRateSwapLegType::Float { spread: _ } => false,
}
}
pub fn get_reference_rate(&self) -> f64 {
match self.swap_type {
InterestRateSwapLegType::Fixed { coupon } => coupon,
InterestRateSwapLegType::Float { spread } => spread,
}
}
pub fn generate_schedule(
&self,
valuation_date: NaiveDate,
) -> Result<Vec<InterestRateSchedulePeriod>> {
if self.schedule.is_empty() {
let effective_date = self.effective_date(valuation_date)?.unwrap();
let tenor_num = match self.schedule_detail.tenor {
Period::Days(num) | Period::Weeks(num) => num as u32,
Period::Months(num) | Period::Years(num) => num,
_ => 1,
};
let duration_num = match self.schedule_detail.duration {
Period::Days(num) | Period::Weeks(num) => num as u32,
Period::Months(num) | Period::Years(num) => num,
_ => 1,
};
let num = duration_num.div_euclid(tenor_num);
let mut schedule = Vec::new();
let mut start_date = effective_date;
for n in 1..(num + 1) {
let end_date = self
.schedule_detail
.calendar
.advance(
effective_date,
(n) * self.schedule_detail.frequency.period().unwrap(),
self.schedule_detail.convention,
Some(self.interest_rate_index.end_of_month),
)?
.unwrap();
let mut irs = InterestRateSchedulePeriod::new(
start_date,
end_date,
self.schedule_detail
.calendar
.advance(
end_date,
Period::Days(self.schedule_detail.pay_delay),
self.schedule_detail.convention,
Some(false),
)?
.unwrap(),
self.schedule_detail
.calendar
.advance(
start_date,
Period::Days(-self.schedule_detail.days_before_accrual),
self.schedule_detail.convention,
Some(false),
)?
.unwrap(),
self.notional,
self.notional,
);
irs.balance = self.notional;
schedule.push(irs);
start_date = end_date;
}
Ok(schedule)
} else {
Ok(self.schedule.to_vec())
}
}
}
#[derive(Deserialize, Serialize, PartialEq, Default, Debug, Copy, Clone)]
pub struct InterestRateSchedulePeriod {
pub accrual_start_date: NaiveDate,
pub accrual_end_date: NaiveDate,
pub pay_date: NaiveDate,
pub reset_date: NaiveDate,
pub amortisation_amounts: f64,
pub balance: f64,
}
impl InterestRateSchedulePeriod {
pub fn new(
accrual_start_date: NaiveDate,
accrual_end_date: NaiveDate,
pay_date: NaiveDate,
reset_date: NaiveDate,
amortisation_amounts: f64,
balance: f64,
) -> Self {
Self {
accrual_start_date,
accrual_end_date,
pay_date,
reset_date,
amortisation_amounts,
balance,
}
}
}
#[derive(Deserialize, Serialize, Debug)]
pub struct InterestRateSwap {
pub legs: Vec<InterestRateSwapLeg>,
}
impl InterestRateSwap {
pub fn new(legs: Vec<InterestRateSwapLeg>) -> Self {
Self { legs }
}
fn amend_last<'a>(
&'a self,
zero_rate: f64,
stripped_curves: &'a mut Vec<StrippedCurve>,
) -> Result<&'a mut Vec<StrippedCurve>> {
stripped_curves.last_mut().unwrap().zero_rate = zero_rate;
Ok(stripped_curves)
}
fn calculate_npv(
&self,
zero_rate: f64,
valuation_date: NaiveDate,
stripped_curves: &mut [StrippedCurve],
) -> Result<f64> {
let new_stripped_curve = &mut stripped_curves.to_vec();
let new_stripped_curve = self.amend_last(zero_rate, new_stripped_curve)?;
let yts = &mut YieldTermStructure::new(
Box::new(Target),
Box::<Actual365Fixed>::default(),
valuation_date,
new_stripped_curve.clone(),
);
self.npv(valuation_date, yts)
}
pub fn solve_zero_rate(
&self,
valuation_date: NaiveDate,
stripped_curves: Vec<StrippedCurve>,
) -> f64 {
let mut convergency = SimpleConvergency {
eps: 1e-15f64,
max_iter: 100,
};
let mut f = |x| {
self.calculate_npv(x, valuation_date, &mut stripped_curves.to_vec())
.unwrap()
};
let root = find_root_brent(0f64, 0.5f64, &mut f, &mut convergency);
root.unwrap()
}
pub fn discount(self, _valuation_date: NaiveDate) -> Result<f64> {
Ok(1f64)
}
pub fn npv(
&self,
valuation_date: NaiveDate,
yield_term_structure: &mut YieldTermStructure,
) -> Result<f64> {
let mut npv = Vec::new();
for leg in &self.legs {
let schedule = leg.generate_schedule(valuation_date)?;
for period in schedule {
let cashflow =
self.calculate_period_cashflow(&period, leg, yield_term_structure)?;
let pv = cashflow.present_value.unwrap();
npv.push(pv);
}
}
let total_npv = npv.iter().sum();
Ok(total_npv)
}
fn calculate_period_cashflow(
&self,
period: &InterestRateSchedulePeriod,
leg: &InterestRateSwapLeg,
yield_term_structure: &mut YieldTermStructure,
) -> Result<InterestRateCashflow> {
let reset_rate = yield_term_structure.forward_rate(
period.reset_date,
leg.schedule_detail.tenor,
&InterpolationMethodEnum::PiecewiseLinearContinuous,
)?;
let discount = yield_term_structure.discount(
period.reset_date,
&InterpolationMethodEnum::PiecewiseLinearContinuous,
)?;
let reset_rate = match leg.swap_type {
InterestRateSwapLegType::Fixed { coupon } => Some(coupon),
InterestRateSwapLegType::Float { spread } => Some(reset_rate + spread),
};
let day_count = leg
.schedule_detail
.day_counter
.day_count(period.accrual_start_date, period.accrual_end_date)?;
let year_fraction = leg
.schedule_detail
.day_counter
.year_fraction(period.accrual_start_date, period.accrual_end_date)?;
Ok(InterestRateCashflow {
day_counts: Some(day_count),
notional: Some(period.balance),
principal: Some(period.balance),
reset_rate,
payment: Some(reset_rate.unwrap_or(0.0) * period.balance),
discount: Some(discount),
present_value: Some(
reset_rate.unwrap_or(0.0)
* year_fraction
* period.balance
* discount
* match leg.direction {
Direction::Buy => 1f64,
Direction::Sell => -1f64,
},
),
})
}
}
impl InterestRateQuote for InterestRateSwap {
fn yts_type(&self) -> InterestRateQuoteEnum {
InterestRateQuoteEnum::Swap
}
fn settle_date(&self, valuation_date: NaiveDate) -> Result<NaiveDate> {
Ok(if self.legs.is_empty() {
valuation_date
} else {
self.legs[0].effective_date(valuation_date)?.unwrap()
})
}
fn maturity_date(&self, valuation_date: NaiveDate) -> Result<NaiveDate> {
let mut last_end_dates = Vec::new();
for leg in &self.legs {
let schedule = if leg.schedule.is_empty() {
&leg.generate_schedule(valuation_date)?
} else {
&leg.schedule
};
last_end_dates.push(schedule.last().unwrap().accrual_end_date);
}
let maturity = if !last_end_dates.is_empty() {
*last_end_dates.iter().max().unwrap()
} else {
valuation_date
};
Ok(maturity)
}
}
#[derive(Deserialize, Serialize, PartialEq, Default, Debug)]
pub struct InterestRateCashflow {
pub day_counts: Option<i64>,
pub notional: Option<f64>,
pub principal: Option<f64>,
pub reset_rate: Option<f64>,
pub payment: Option<f64>,
pub discount: Option<f64>,
pub present_value: Option<f64>,
}
#[cfg(test)]
mod tests {
use super::{InterestRateSwap, InterestRateSwapLegType, ScheduleDetail};
use crate::derivatives::basic::Direction;
use crate::derivatives::interestrate::swap::InterestRateSwapLeg;
use crate::error::Result;
use crate::markets::interestrate::interestrateindex::{
InterestRateIndex, InterestRateIndexEnum,
};
use crate::markets::termstructures::yieldcurve::{
InterestRateQuoteEnum, StrippedCurve, YieldTermStructure,
};
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendars::Target;
use crate::time::daycounters::actual360::Actual360;
use crate::time::daycounters::actual365fixed::Actual365Fixed;
use crate::time::daycounters::thirty360::Thirty360;
use crate::time::frequency::Frequency;
use crate::time::period::Period;
use chrono::NaiveDate;
#[test]
fn test_none_schedule() -> Result<()> {
let mut random_irs = InterestRateSwap::new(vec![
InterestRateSwapLeg::new(
InterestRateSwapLegType::Fixed { coupon: 0.0330800 },
Direction::Buy,
InterestRateIndex::from_enum(InterestRateIndexEnum::EUIBOR(Period::Months(3)))
.unwrap(),
1f64,
ScheduleDetail::new(
Frequency::Annual,
Period::SPOT,
Period::SPOT,
Box::new(Thirty360::default()),
Box::<Target>::default(),
BusinessDayConvention::ModifiedFollowing,
2,
0i64,
0i64,
),
vec![],
),
InterestRateSwapLeg::new(
InterestRateSwapLegType::Float { spread: 0f64 },
Direction::Sell,
InterestRateIndex::from_enum(InterestRateIndexEnum::EUIBOR(Period::Months(3)))
.unwrap(),
1f64,
ScheduleDetail::new(
Frequency::Quarterly,
Period::Months(3),
Period::Months(12),
Box::new(Thirty360::default()),
Box::<Target>::default(),
BusinessDayConvention::ModifiedFollowing,
2,
0i64,
0i64,
),
vec![],
),
]);
let valuation_date = NaiveDate::from_ymd_opt(2023, 10, 27).unwrap();
for leg in &mut random_irs.legs {
leg.generate_schedule(valuation_date)?;
}
assert_eq!(random_irs.legs.get(0).unwrap().schedule, vec![]);
assert_eq!(random_irs.legs.get(1).unwrap().schedule, vec![]);
Ok(())
}
#[test]
fn test_week_schedule() -> Result<()> {
let mut random_irs = InterestRateSwap::new(vec![
InterestRateSwapLeg::new(
InterestRateSwapLegType::Fixed { coupon: 0.0330800 },
Direction::Buy,
InterestRateIndex::from_enum(InterestRateIndexEnum::EUIBOR(Period::Months(3)))
.unwrap(),
1f64,
ScheduleDetail::new(
Frequency::Weekly,
Period::Weeks(1),
Period::Weeks(1),
Box::new(Thirty360::default()),
Box::<Target>::default(),
BusinessDayConvention::ModifiedFollowing,
2,
0i64,
0i64,
),
vec![],
),
InterestRateSwapLeg::new(
InterestRateSwapLegType::Float { spread: 0f64 },
Direction::Sell,
InterestRateIndex::from_enum(InterestRateIndexEnum::EUIBOR(Period::Months(3)))
.unwrap(),
1f64,
ScheduleDetail::new(
Frequency::Weekly,
Period::Weeks(1),
Period::Weeks(1),
Box::new(Thirty360::default()),
Box::<Target>::default(),
BusinessDayConvention::ModifiedFollowing,
2,
0i64,
0i64,
),
vec![],
),
]);
let valuation_date = NaiveDate::from_ymd_opt(2023, 10, 27).unwrap();
for leg in random_irs.legs.iter_mut() {
leg.generate_schedule(valuation_date)?;
}
let fix_schedule = &random_irs
.legs
.get(0)
.unwrap()
.generate_schedule(valuation_date)?;
let float_schedule = &random_irs
.legs
.get(1)
.unwrap()
.generate_schedule(valuation_date)?;
assert_eq!(
fix_schedule.get(0).unwrap().accrual_end_date,
NaiveDate::from_ymd_opt(2023, 11, 7).unwrap()
);
assert_eq!(
float_schedule.get(0).unwrap().accrual_end_date,
NaiveDate::from_ymd_opt(2023, 11, 7).unwrap()
);
Ok(())
}
#[test]
fn test_eusw3v3_schedule() -> Result<()> {
let valuation_date = NaiveDate::from_ymd_opt(2023, 10, 27).unwrap();
let yts = &mut YieldTermStructure::new(
Box::new(Target::default()),
Box::new(Actual365Fixed::default()),
valuation_date,
vec![
StrippedCurve {
first_settle_date: NaiveDate::from_ymd_opt(2023, 10, 31).unwrap(),
date: NaiveDate::from_ymd_opt(2024, 1, 31).unwrap(),
market_rate: 0.03948,
zero_rate: 0.0398278,
discount: 0.989579,
source: InterestRateQuoteEnum::OIS,
hidden_pillar: false,
},
StrippedCurve {
first_settle_date: NaiveDate::from_ymd_opt(2023, 11, 15).unwrap(),
date: NaiveDate::from_ymd_opt(2024, 2, 21).unwrap(),
market_rate: 0.0395485,
zero_rate: 0.0398744,
discount: 0.987330,
source: InterestRateQuoteEnum::Futures,
hidden_pillar: false,
},
StrippedCurve {
first_settle_date: NaiveDate::from_ymd_opt(2023, 12, 20).unwrap(),
date: NaiveDate::from_ymd_opt(2024, 3, 20).unwrap(),
market_rate: 0.0396444,
zero_rate: 0.0399327,
discount: 0.984261,
source: InterestRateQuoteEnum::Futures,
hidden_pillar: false,
},
StrippedCurve {
first_settle_date: NaiveDate::from_ymd_opt(2024, 1, 17).unwrap(),
date: NaiveDate::from_ymd_opt(2024, 4, 17).unwrap(),
market_rate: 0.0395403,
zero_rate: 0.0398607,
discount: 0.981284,
source: InterestRateQuoteEnum::Futures,
hidden_pillar: false,
},
StrippedCurve {
first_settle_date: NaiveDate::from_ymd_opt(2024, 2, 21).unwrap(),
date: NaiveDate::from_ymd_opt(2024, 5, 15).unwrap(),
market_rate: 0.0389848,
zero_rate: 0.0398542,
discount: 0.9784,
source: InterestRateQuoteEnum::Futures,
hidden_pillar: false,
},
StrippedCurve {
first_settle_date: NaiveDate::from_ymd_opt(2024, 3, 20).unwrap(),
date: NaiveDate::from_ymd_opt(2024, 6, 19).unwrap(),
market_rate: 0.0384784,
zero_rate: 0.0395053,
discount: 0.97478,
source: InterestRateQuoteEnum::Futures,
hidden_pillar: false,
},
StrippedCurve {
first_settle_date: NaiveDate::from_ymd_opt(2024, 4, 17).unwrap(),
date: NaiveDate::from_ymd_opt(2024, 7, 17).unwrap(),
market_rate: 0.0378718,
zero_rate: 0.0392935,
discount: 0.97198,
source: InterestRateQuoteEnum::Futures,
hidden_pillar: false,
},
StrippedCurve {
first_settle_date: NaiveDate::from_ymd_opt(2024, 6, 19).unwrap(),
date: NaiveDate::from_ymd_opt(2024, 9, 18).unwrap(),
market_rate: 0.0364545,
zero_rate: 0.0387501,
discount: 0.96588,
source: InterestRateQuoteEnum::Futures,
hidden_pillar: false,
},
StrippedCurve {
first_settle_date: NaiveDate::from_ymd_opt(2024, 9, 18).unwrap(),
date: NaiveDate::from_ymd_opt(2024, 12, 18).unwrap(),
market_rate: 0.0340233,
zero_rate: 0.0377918,
discount: 0.957644,
source: InterestRateQuoteEnum::Futures,
hidden_pillar: false,
},
StrippedCurve {
first_settle_date: NaiveDate::from_ymd_opt(2024, 12, 18).unwrap(),
date: NaiveDate::from_ymd_opt(2025, 3, 19).unwrap(),
market_rate: 0.0317351,
zero_rate: 0.0367648,
discount: 0.950023,
source: InterestRateQuoteEnum::Futures,
hidden_pillar: false,
},
StrippedCurve {
first_settle_date: NaiveDate::from_ymd_opt(2025, 3, 19).unwrap(),
date: NaiveDate::from_ymd_opt(2025, 6, 18).unwrap(),
market_rate: 0.0299895,
zero_rate: 0.035783,
discount: 0.942875,
source: InterestRateQuoteEnum::Futures,
hidden_pillar: false,
},
StrippedCurve {
first_settle_date: NaiveDate::from_ymd_opt(2025, 6, 18).unwrap(),
date: NaiveDate::from_ymd_opt(2025, 9, 17).unwrap(),
market_rate: 0.0288871,
zero_rate: 0.0349137,
discount: 0.936040,
source: InterestRateQuoteEnum::Futures,
hidden_pillar: false,
},
StrippedCurve {
first_settle_date: NaiveDate::from_ymd_opt(2025, 9, 17).unwrap(),
date: NaiveDate::from_ymd_opt(2025, 12, 17).unwrap(),
market_rate: 0.0283781,
zero_rate: 0.0341871,
discount: 0.929373,
source: InterestRateQuoteEnum::Futures,
hidden_pillar: false,
},
StrippedCurve {
first_settle_date: NaiveDate::from_ymd_opt(2025, 12, 17).unwrap(),
date: NaiveDate::from_ymd_opt(2026, 3, 18).unwrap(),
market_rate: 0.0282122,
zero_rate: 0.0335945,
discount: 0.922793,
source: InterestRateQuoteEnum::Futures,
hidden_pillar: false,
},
StrippedCurve {
first_settle_date: NaiveDate::from_ymd_opt(2026, 3, 18).unwrap(),
date: NaiveDate::from_ymd_opt(2026, 6, 17).unwrap(),
market_rate: 0.0282401,
zero_rate: 0.0331165,
discount: 0.916252,
source: InterestRateQuoteEnum::Futures,
hidden_pillar: false,
},
StrippedCurve {
first_settle_date: NaiveDate::from_ymd_opt(2026, 6, 17).unwrap(),
date: NaiveDate::from_ymd_opt(2026, 9, 16).unwrap(),
market_rate: 0.0283114,
zero_rate: 0.0327271,
discount: 0.909741,
source: InterestRateQuoteEnum::Futures,
hidden_pillar: false,
},
StrippedCurve {
first_settle_date: NaiveDate::from_ymd_opt(2026, 9, 16).unwrap(),
date: NaiveDate::from_ymd_opt(2026, 12, 16).unwrap(),
market_rate: 0.0284768,
zero_rate: 0.0324128,
discount: 0.903240,
source: InterestRateQuoteEnum::Futures,
hidden_pillar: false,
},
StrippedCurve {
first_settle_date: NaiveDate::from_ymd_opt(2023, 10, 29).unwrap(),
date: NaiveDate::from_ymd_opt(2027, 10, 29).unwrap(),
market_rate: 0.0322925,
zero_rate: 0.0317946,
discount: 0.880664,
source: InterestRateQuoteEnum::Swap,
hidden_pillar: false,
},
],
);
let mut eusw3v3 = InterestRateSwap::new(vec![
InterestRateSwapLeg::new(
InterestRateSwapLegType::Fixed { coupon: 0.03308 },
Direction::Buy,
InterestRateIndex::from_enum(InterestRateIndexEnum::EUIBOR(Period::Months(3)))
.unwrap(),
1f64,
ScheduleDetail::new(
Frequency::Annual,
Period::Years(1),
Period::Years(3),
Box::new(Thirty360::default()),
Box::<Target>::default(),
BusinessDayConvention::ModifiedFollowing,
2,
0i64,
0i64,
),
vec![],
),
InterestRateSwapLeg::new(
InterestRateSwapLegType::Float { spread: 0f64 },
Direction::Sell,
InterestRateIndex::from_enum(InterestRateIndexEnum::EUIBOR(Period::Months(3)))
.unwrap(),
1f64,
ScheduleDetail::new(
Frequency::Quarterly,
Period::Months(3),
Period::Months(36),
Box::new(Actual360),
Box::<Target>::default(),
BusinessDayConvention::ModifiedFollowing,
2,
0i64,
0i64,
),
vec![],
),
]);
for leg in eusw3v3.legs.iter_mut() {
leg.schedule = leg.generate_schedule(valuation_date)?;
}
{
let legs = &eusw3v3.legs;
let fixed_schedule = &legs.get(0).unwrap().generate_schedule(valuation_date)?;
let float_schedule = &legs.get(1).unwrap().generate_schedule(valuation_date)?;
assert_eq!(fixed_schedule.len(), 3);
assert_eq!(float_schedule.len(), 12);
let mut expected_fixed_dates = Vec::new();
expected_fixed_dates.push(NaiveDate::from_ymd_opt(2024, 10, 31).unwrap());
expected_fixed_dates.push(NaiveDate::from_ymd_opt(2025, 10, 31).unwrap());
expected_fixed_dates.push(NaiveDate::from_ymd_opt(2026, 10, 30).unwrap());
let mut n = 0;
for period in fixed_schedule.iter() {
assert_eq!(period.accrual_end_date, expected_fixed_dates[n]);
n += 1;
}
let mut expected_float_dates = Vec::new();
expected_float_dates.push(NaiveDate::from_ymd_opt(2024, 1, 31).unwrap());
expected_float_dates.push(NaiveDate::from_ymd_opt(2024, 4, 30).unwrap());
expected_float_dates.push(NaiveDate::from_ymd_opt(2024, 7, 31).unwrap());
expected_float_dates.push(NaiveDate::from_ymd_opt(2024, 10, 31).unwrap());
expected_float_dates.push(NaiveDate::from_ymd_opt(2025, 1, 31).unwrap());
expected_float_dates.push(NaiveDate::from_ymd_opt(2025, 4, 30).unwrap());
expected_float_dates.push(NaiveDate::from_ymd_opt(2025, 7, 31).unwrap());
expected_float_dates.push(NaiveDate::from_ymd_opt(2025, 10, 31).unwrap());
expected_float_dates.push(NaiveDate::from_ymd_opt(2026, 1, 30).unwrap());
expected_float_dates.push(NaiveDate::from_ymd_opt(2026, 4, 30).unwrap());
expected_float_dates.push(NaiveDate::from_ymd_opt(2026, 7, 31).unwrap());
expected_float_dates.push(NaiveDate::from_ymd_opt(2026, 10, 30).unwrap());
for n in 0..float_schedule.len() {
assert_eq!(float_schedule[n].accrual_end_date, expected_float_dates[n])
}
}
assert_eq!(
format!("{:.2}", (eusw3v3.npv(valuation_date, yts)?)),
"0.00"
);
Ok(())
}
}