use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use crate::derivatives::basic::Direction;
use crate::derivatives::forex::basic::CurrencyValue;
use crate::derivatives::interestrate::basic::{IRDerivatives, RateShiftMode};
use crate::error::{Error, Result};
use crate::markets::interestrate::interestrateindex::InterestRateIndex;
use crate::markets::interestrate::market_context::IrMarketContext;
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 iso_currency::Currency;
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 total_npv = 0f64;
for leg in &self.legs {
let schedule = leg.generate_schedule(valuation_date)?;
let direction_sign = match leg.direction {
Direction::Buy => 1f64,
Direction::Sell => -1f64,
};
for period in &schedule {
let cashflow = self.calculate_period_cashflow(period, leg, yield_term_structure)?;
total_npv += cashflow.present_value.unwrap();
}
if let Some(last) = schedule.last() {
let df_last = yield_term_structure.discount(
last.pay_date,
&InterpolationMethodEnum::PiecewiseLinearContinuous,
)?;
total_npv += last.balance * df_last * direction_sign;
}
}
Ok(total_npv)
}
fn calculate_period_cashflow(
&self,
period: &InterestRateSchedulePeriod,
leg: &InterestRateSwapLeg,
yield_term_structure: &mut YieldTermStructure,
) -> Result<InterestRateCashflow> {
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)?;
let reset_rate = match leg.swap_type {
InterestRateSwapLegType::Fixed { coupon } => coupon,
InterestRateSwapLegType::Float { spread } => {
let df_start = yield_term_structure.discount(
period.accrual_start_date,
&InterpolationMethodEnum::PiecewiseLinearContinuous,
)?;
let df_end = yield_term_structure.discount(
period.accrual_end_date,
&InterpolationMethodEnum::PiecewiseLinearContinuous,
)?;
(df_start / df_end - 1.0) / year_fraction + spread
}
};
let discount = yield_term_structure.discount(
period.pay_date,
&InterpolationMethodEnum::PiecewiseLinearContinuous,
)?;
let direction_sign = match leg.direction {
Direction::Buy => 1f64,
Direction::Sell => -1f64,
};
let payment = reset_rate * year_fraction * period.balance;
Ok(InterestRateCashflow {
day_counts: Some(day_count),
notional: Some(period.balance),
principal: Some(period.balance),
reset_rate: Some(reset_rate),
payment: Some(payment),
discount: Some(discount),
present_value: Some(payment * discount * direction_sign),
})
}
pub fn currency(&self) -> Result<Currency> {
self.legs
.first()
.map(|leg| leg.interest_rate_index.currency)
.ok_or_else(|| Error::InvalidData("InterestRateSwap has no legs".to_string()))
}
fn pv_under_shift(
&self,
valuation_date: NaiveDate,
yts: &YieldTermStructure,
rate_shift_bp: f64,
) -> Result<f64> {
let method = &InterpolationMethodEnum::PiecewiseLinearContinuous;
let mut total_npv = 0.0_f64;
for leg in &self.legs {
let schedule = leg.generate_schedule(valuation_date)?;
let direction_sign = match leg.direction {
Direction::Buy => 1.0_f64,
Direction::Sell => -1.0_f64,
};
for period in &schedule {
let year_fraction = leg
.schedule_detail
.day_counter
.year_fraction(period.accrual_start_date, period.accrual_end_date)?;
let reset_rate = match leg.swap_type {
InterestRateSwapLegType::Fixed { coupon } => coupon,
InterestRateSwapLegType::Float { spread } => {
let df_start =
yts.shifted_discount(period.accrual_start_date, method, rate_shift_bp)?;
let df_end =
yts.shifted_discount(period.accrual_end_date, method, rate_shift_bp)?;
(df_start / df_end - 1.0) / year_fraction + spread
}
};
let df_pay = yts.shifted_discount(period.pay_date, method, rate_shift_bp)?;
let payment = reset_rate * year_fraction * period.balance;
total_npv += payment * df_pay * direction_sign;
}
if let Some(last) = schedule.last() {
let df_last = yts.shifted_discount(last.pay_date, method, rate_shift_bp)?;
total_npv += last.balance * df_last * direction_sign;
}
}
Ok(total_npv)
}
}
impl IRDerivatives for InterestRateSwap {
fn mtm(&self, market: &IrMarketContext) -> Result<CurrencyValue> {
let pv = self.pv_under_shift(market.valuation_date, &market.curve, 0.0)?;
Ok(CurrencyValue {
currency: self.currency()?,
value: pv,
})
}
fn dv01(&self, market: &IrMarketContext) -> Result<f64> {
let base = self.pv_under_shift(market.valuation_date, &market.curve, 0.0)?;
let up = self.pv_under_shift(market.valuation_date, &market.curve, 1.0)?;
Ok(up - base)
}
fn gamma(
&self,
market: &IrMarketContext,
rate_shift_bp: f64,
mode: RateShiftMode,
) -> Result<f64> {
match mode {
RateShiftMode::Zeros => {}
other => {
return Err(Error::InvalidData(format!(
"rate shift mode {:?} is not yet implemented; only Zeros is supported",
other
)));
}
}
let base = self.pv_under_shift(market.valuation_date, &market.curve, 0.0)?;
let up = self.pv_under_shift(market.valuation_date, &market.curve, rate_shift_bp)?;
let down = self.pv_under_shift(market.valuation_date, &market.curve, -rate_shift_bp)?;
Ok(up + down - 2.0 * base)
}
fn vega(&self, _market: &IrMarketContext, _vol_shift_bp: f64) -> Result<f64> {
Ok(0.0)
}
}
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::{
InterestRateSchedulePeriod, 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::oisratehelper::OISRate;
use crate::markets::termstructures::yieldcurve::{
InterestRateQuoteEnum, InterpolationMethodEnum, StrippedCurve, YieldTermMarketData,
YieldTermStructure,
};
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendars::unitedstates::UnitedStatesMarket;
use crate::time::calendars::{Target, UnitedStates};
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;
use iso_currency::Currency;
#[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])
}
}
let eusw3v3_npv = eusw3v3.npv(valuation_date, yts)?;
assert!(
eusw3v3_npv.abs() < 0.05,
"EUSW3V3 NPV drifted beyond tolerance: {}",
eusw3v3_npv,
);
Ok(())
}
fn usd_sofr_index(period: Period) -> InterestRateIndex {
InterestRateIndex {
period,
settlement_days: 2,
currency: Currency::USD,
calendar: Box::new(UnitedStates {
market: Some(UnitedStatesMarket::SOFR),
}),
convention: BusinessDayConvention::ModifiedFollowing,
day_counter: Box::new(Actual360),
end_of_month: false,
}
}
fn usd_sofr_ois(period: Period, rate: f64) -> OISRate {
OISRate {
value: rate,
interest_rate_index: usd_sofr_index(period),
}
}
fn usd_sofr_swap_quote(tenor_years: u32, rate: f64) -> InterestRateSwap {
let make_leg =
|direction: Direction, leg_type: InterestRateSwapLegType| -> InterestRateSwapLeg {
InterestRateSwapLeg::new(
leg_type,
direction,
usd_sofr_index(Period::Years(1)),
1.0,
ScheduleDetail::new(
Frequency::Annual,
Period::Years(1),
Period::Years(tenor_years),
Box::new(Actual360),
Box::new(UnitedStates {
market: Some(UnitedStatesMarket::SOFR),
}),
BusinessDayConvention::ModifiedFollowing,
2,
0,
0,
),
vec![],
)
};
InterestRateSwap::new(vec![
make_leg(
Direction::Buy,
InterestRateSwapLegType::Fixed { coupon: rate },
),
make_leg(
Direction::Sell,
InterestRateSwapLegType::Float { spread: 0.0 },
),
])
}
fn usd_sofr_market_data(valuation_date: NaiveDate) -> YieldTermMarketData {
let ois_quotes = vec![
usd_sofr_ois(Period::Weeks(1), 0.0364150),
usd_sofr_ois(Period::Months(1), 0.0365110),
usd_sofr_ois(Period::Months(3), 0.0366790),
usd_sofr_ois(Period::Months(6), 0.0368070),
usd_sofr_ois(Period::Months(12), 0.0368800),
];
let swap_quotes = vec![
usd_sofr_swap_quote(2, 0.0360645),
usd_sofr_swap_quote(4, 0.0357510),
usd_sofr_swap_quote(5, 0.0360743),
usd_sofr_swap_quote(6, 0.0365400),
];
YieldTermMarketData::new(valuation_date, ois_quotes, vec![], swap_quotes)
}
#[test]
fn test_usd_sofr_curve_and_fixed_leg_expected_reference() -> Result<()> {
let valuation_date = NaiveDate::from_ymd_opt(2026, 4, 21).unwrap();
let market_data = usd_sofr_market_data(valuation_date);
let stripped_curves = market_data.get_stripped_curve()?;
let yts = YieldTermStructure::new(
Box::new(UnitedStates::default()),
Box::new(Actual365Fixed::default()),
valuation_date,
stripped_curves,
);
let fixed_cashflows: &[(NaiveDate, i64, f64, f64)] = &[
(
NaiveDate::from_ymd_opt(2027, 4, 27).unwrap(),
365,
0.963579,
360_303.69,
),
(
NaiveDate::from_ymd_opt(2028, 4, 26).unwrap(),
367,
0.930398,
349_802.84,
),
(
NaiveDate::from_ymd_opt(2029, 4, 25).unwrap(),
364,
0.898729,
335_134.09,
),
(
NaiveDate::from_ymd_opt(2030, 4, 25).unwrap(),
365,
0.867044,
324_207.14,
),
(
NaiveDate::from_ymd_opt(2031, 4, 25).unwrap(),
365,
0.835286,
8_665_192.90,
),
];
for &(pay_date, _, bb_df, _) in fixed_cashflows {
let our_df = yts.discount(pay_date, &InterpolationMethodEnum::StepFunctionForward)?;
let diff = (our_df - bb_df).abs();
assert!(
diff < 2.0e-3,
"DF at {} drifted {:.6} vs Expected {:.6} (|Δ|={:.6})",
pay_date,
our_df,
bb_df,
diff,
);
}
let notional = 10_000_000.0f64;
let coupon = 0.036880f64;
let mut leg1_pv = 0.0f64;
for (i, &(pay_date, accr_days, _, _)) in fixed_cashflows.iter().enumerate() {
let df = yts.discount(pay_date, &InterpolationMethodEnum::StepFunctionForward)?;
let coupon_cf = notional * coupon * (accr_days as f64) / 360.0;
let principal_cf = if i == fixed_cashflows.len() - 1 {
notional
} else {
0.0
};
leg1_pv += (coupon_cf + principal_cf) * df;
}
let expected_leg1_pv = 10_034_640.66f64;
let abs_err = (leg1_pv - expected_leg1_pv).abs();
assert!(
abs_err < 25_000.0,
"Fixed-leg PV {:.2} off by {:.2} from Expected {:.2}",
leg1_pv,
abs_err,
expected_leg1_pv,
);
Ok(())
}
fn expected_5y_sofr_schedule() -> Vec<InterestRateSchedulePeriod> {
let notional = 10_000_000.0;
let d = |y, m, d| NaiveDate::from_ymd_opt(y, m, d).unwrap();
vec![
InterestRateSchedulePeriod {
accrual_start_date: d(2026, 4, 23),
accrual_end_date: d(2027, 4, 23),
pay_date: d(2027, 4, 27),
reset_date: d(2026, 4, 23),
amortisation_amounts: 0.0,
balance: notional,
},
InterestRateSchedulePeriod {
accrual_start_date: d(2027, 4, 23),
accrual_end_date: d(2028, 4, 24),
pay_date: d(2028, 4, 26),
reset_date: d(2027, 4, 23),
amortisation_amounts: 0.0,
balance: notional,
},
InterestRateSchedulePeriod {
accrual_start_date: d(2028, 4, 24),
accrual_end_date: d(2029, 4, 23),
pay_date: d(2029, 4, 25),
reset_date: d(2028, 4, 24),
amortisation_amounts: 0.0,
balance: notional,
},
InterestRateSchedulePeriod {
accrual_start_date: d(2029, 4, 23),
accrual_end_date: d(2030, 4, 23),
pay_date: d(2030, 4, 25),
reset_date: d(2029, 4, 23),
amortisation_amounts: 0.0,
balance: notional,
},
InterestRateSchedulePeriod {
accrual_start_date: d(2030, 4, 23),
accrual_end_date: d(2031, 4, 23),
pay_date: d(2031, 4, 25),
reset_date: d(2030, 4, 23),
amortisation_amounts: 0.0,
balance: notional,
},
]
}
#[test]
fn test_usd_sofr_5y_swap_npv_expected_reference() -> Result<()> {
let valuation_date = NaiveDate::from_ymd_opt(2026, 4, 21).unwrap();
let market_data = usd_sofr_market_data(valuation_date);
let stripped_curves = market_data.get_stripped_curve()?;
let yts = &mut YieldTermStructure::new(
Box::new(UnitedStates::default()),
Box::new(Actual365Fixed::default()),
valuation_date,
stripped_curves,
);
let schedule = expected_5y_sofr_schedule();
let fixed_leg = InterestRateSwapLeg::new(
InterestRateSwapLegType::Fixed { coupon: 0.036880 },
Direction::Buy,
usd_sofr_index(Period::Years(1)),
10_000_000.0,
ScheduleDetail::new(
Frequency::Annual,
Period::Years(1),
Period::Years(5),
Box::new(Actual360),
Box::new(UnitedStates::default()),
BusinessDayConvention::ModifiedFollowing,
2,
2,
0,
),
schedule.clone(),
);
let float_leg = InterestRateSwapLeg::new(
InterestRateSwapLegType::Float { spread: 0.0 },
Direction::Sell,
usd_sofr_index(Period::Years(1)),
10_000_000.0,
ScheduleDetail::new(
Frequency::Annual,
Period::Years(1),
Period::Years(5),
Box::new(Actual360),
Box::new(UnitedStates::default()),
BusinessDayConvention::ModifiedFollowing,
2,
2,
0,
),
schedule,
);
let swap = InterestRateSwap::new(vec![fixed_leg, float_leg]);
let net_npv = swap.npv(valuation_date, yts)?;
let expected_net_npv = 36_739.97f64;
let abs_err = (net_npv - expected_net_npv).abs();
assert!(
abs_err < 8_000.0,
"Net NPV {:.2} off by {:.2} from Expected {:.2}",
net_npv,
abs_err,
expected_net_npv,
);
Ok(())
}
#[test]
fn ir_derivatives_trait_on_usd_sofr_5y_swap() -> Result<()> {
use crate::derivatives::interestrate::basic::{
DEFAULT_RATE_SHIFT_BP, IRDerivatives, RateShiftMode,
};
use crate::markets::interestrate::market_context::IrMarketContext;
let valuation_date = NaiveDate::from_ymd_opt(2026, 4, 21).unwrap();
let market_data = usd_sofr_market_data(valuation_date);
let stripped_curves = market_data.get_stripped_curve()?;
let yts = YieldTermStructure::new(
Box::new(UnitedStates::default()),
Box::new(Actual365Fixed::default()),
valuation_date,
stripped_curves,
);
let schedule = expected_5y_sofr_schedule();
let fixed_leg = InterestRateSwapLeg::new(
InterestRateSwapLegType::Fixed { coupon: 0.036880 },
Direction::Buy,
usd_sofr_index(Period::Years(1)),
10_000_000.0,
ScheduleDetail::new(
Frequency::Annual,
Period::Years(1),
Period::Years(5),
Box::new(Actual360),
Box::new(UnitedStates::default()),
BusinessDayConvention::ModifiedFollowing,
2,
2,
0,
),
schedule.clone(),
);
let float_leg = InterestRateSwapLeg::new(
InterestRateSwapLegType::Float { spread: 0.0 },
Direction::Sell,
usd_sofr_index(Period::Years(1)),
10_000_000.0,
ScheduleDetail::new(
Frequency::Annual,
Period::Years(1),
Period::Years(5),
Box::new(Actual360),
Box::new(UnitedStates::default()),
BusinessDayConvention::ModifiedFollowing,
2,
2,
0,
),
schedule,
);
let swap = InterestRateSwap::new(vec![fixed_leg, float_leg]);
let ctx = IrMarketContext::new(valuation_date, Currency::USD, yts, None);
let mtm = swap.mtm(&ctx)?;
assert_eq!(mtm.currency, Currency::USD);
assert!(
mtm.value.abs() < 1_000_000.0,
"mtm {} implausibly large for ATM-ish 5 Y IRS",
mtm.value
);
assert!(
mtm.value > 0.0,
"receive-fixed mtm should be positive at 3.688% coupon"
);
let dv01 = swap.dv01(&ctx)?;
assert!(
dv01 < 0.0,
"receive-fixed DV01 should be negative, got {}",
dv01
);
assert!(
dv01.abs() > 100.0 && dv01.abs() < 10_000.0,
"DV01 {} outside expected O($1k) band for 5 Y / 10 MM IRS",
dv01,
);
let gamma10 = swap.gamma(&ctx, DEFAULT_RATE_SHIFT_BP, RateShiftMode::default())?;
assert!(gamma10.abs() < 1_000.0, "|gamma10bp| {} too large", gamma10);
let vega = swap.vega(&ctx, 1.0)?;
assert_eq!(vega, 0.0);
let mod_dur = swap.modified_duration(&ctx)?;
assert!(
mod_dur > 0.0,
"receive-fixed mod duration should be > 0, got {}",
mod_dur
);
for mode in [
RateShiftMode::Instruments,
RateShiftMode::Forwards,
RateShiftMode::Swaps,
] {
assert!(swap.gamma(&ctx, DEFAULT_RATE_SHIFT_BP, mode).is_err());
}
Ok(())
}
}