use crate::cashflow::{CashFlow, Leg};
use crate::cashflows::overnightindexedcoupon::OvernightIndexedCoupon;
use crate::cashflows::rateaveraging::RateAveraging;
use crate::errors::QlResult;
use crate::indexes::iborindex::OvernightIndex;
use crate::require;
use crate::shared::{Shared, shared};
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendar::Calendar;
use crate::time::daycounter::DayCounter;
use crate::time::schedule::Schedule;
use crate::time::timeunit::TimeUnit;
use crate::types::{Integer, Real, Spread};
#[must_use]
pub struct OvernightLeg {
schedule: Schedule,
index: Shared<OvernightIndex>,
notionals: Vec<Real>,
payment_day_counter: Option<DayCounter>,
payment_adjustment: BusinessDayConvention,
payment_lag: Integer,
payment_calendar: Calendar,
gearings: Vec<Real>,
spreads: Vec<Spread>,
averaging_method: RateAveraging,
compound_spread_daily: bool,
}
impl OvernightLeg {
pub fn new(schedule: Schedule, index: Shared<OvernightIndex>) -> OvernightLeg {
let payment_calendar = schedule.calendar().clone();
OvernightLeg {
schedule,
index,
notionals: Vec::new(),
payment_day_counter: None,
payment_adjustment: BusinessDayConvention::Following,
payment_lag: 0,
payment_calendar,
gearings: Vec::new(),
spreads: Vec::new(),
averaging_method: RateAveraging::Compound,
compound_spread_daily: false,
}
}
pub fn with_notional(self, notional: Real) -> OvernightLeg {
self.with_notionals(vec![notional])
}
pub fn with_notionals(mut self, notionals: Vec<Real>) -> OvernightLeg {
self.notionals = notionals;
self
}
pub fn with_payment_day_counter(mut self, day_counter: DayCounter) -> OvernightLeg {
self.payment_day_counter = Some(day_counter);
self
}
pub fn with_payment_adjustment(mut self, convention: BusinessDayConvention) -> OvernightLeg {
self.payment_adjustment = convention;
self
}
pub fn with_payment_lag(mut self, lag: Integer) -> OvernightLeg {
self.payment_lag = lag;
self
}
pub fn with_payment_calendar(mut self, calendar: Calendar) -> OvernightLeg {
self.payment_calendar = calendar;
self
}
pub fn with_gearing(self, gearing: Real) -> OvernightLeg {
self.with_gearings(vec![gearing])
}
pub fn with_gearings(mut self, gearings: Vec<Real>) -> OvernightLeg {
self.gearings = gearings;
self
}
pub fn with_spread(self, spread: Spread) -> OvernightLeg {
self.with_spreads(vec![spread])
}
pub fn with_spreads(mut self, spreads: Vec<Spread>) -> OvernightLeg {
self.spreads = spreads;
self
}
pub fn with_averaging_method(mut self, averaging_method: RateAveraging) -> OvernightLeg {
self.averaging_method = averaging_method;
self
}
pub fn with_compound_spread_daily(mut self, compound_spread_daily: bool) -> OvernightLeg {
self.compound_spread_daily = compound_spread_daily;
self
}
pub fn coupons(&self) -> QlResult<Vec<Shared<OvernightIndexedCoupon>>> {
require!(!self.notionals.is_empty(), "no notional given");
let size = self.schedule.len();
require!(size >= 2, "schedule with {size} date(s) spans no period");
let periods = size - 1;
require!(
self.notionals.len() <= periods,
"too many notionals ({}), only {periods} required",
self.notionals.len()
);
require!(
self.gearings.len() <= periods,
"too many gearings ({}), only {periods} required",
self.gearings.len()
);
require!(
self.spreads.len() <= periods,
"too many spreads ({}), only {periods} required",
self.spreads.len()
);
let calendar = self.schedule.calendar();
let stub = |period: usize| {
self.schedule.has_tenor()
&& self.schedule.has_is_regular()
&& !self.schedule.is_regular_at(period)
};
let mut coupons = Vec::with_capacity(periods);
for i in 0..periods {
let start = self.schedule.date(i);
let end = self.schedule.date(i + 1);
let mut reference_start = start;
let mut reference_end = end;
if i == 0 && stub(1) {
reference_start = calendar.advance_by_period(
end,
-self.schedule.tenor(),
self.payment_adjustment,
false,
);
}
if i == periods - 1 && stub(i + 1) {
reference_end = calendar.advance_by_period(
start,
self.schedule.tenor(),
self.payment_adjustment,
false,
);
}
let payment_date = self.payment_calendar.advance(
end,
self.payment_lag,
TimeUnit::Days,
self.payment_adjustment,
false,
);
let coupon = OvernightIndexedCoupon::new(
payment_date,
broadcast(&self.notionals, i, 1.0),
start,
end,
self.index.clone(),
broadcast(&self.gearings, i, 1.0),
broadcast(&self.spreads, i, 0.0),
Some(reference_start),
Some(reference_end),
self.payment_day_counter.clone(),
self.averaging_method,
self.compound_spread_daily,
None,
)?;
coupons.push(shared(coupon));
}
Ok(coupons)
}
pub fn build(&self) -> QlResult<Leg> {
Ok(self
.coupons()?
.into_iter()
.map(|coupon| coupon as Shared<dyn CashFlow>)
.collect())
}
}
fn broadcast<T: Clone>(values: &[T], index: usize, default: T) -> T {
match values.last() {
None => default,
Some(last) => values.get(index).unwrap_or(last).clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cashflows::coupon::Coupon;
use crate::handle::RelinkableHandle;
use crate::indexes::ibor::Sofr;
use crate::interestrate::Compounding;
use crate::settings::Settings;
use crate::termstructures::yields::FlatForward;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::calendars::unitedstates::{Market, UnitedStates};
use crate::time::date::{Date, Month};
use crate::time::daycounters::actual360::Actual360;
use crate::time::frequency::Frequency;
use crate::time::schedule::MakeSchedule;
use crate::types::Rate;
const NOTIONAL: Real = 1_000_000.0;
fn common_vars(
today: Date,
) -> (
Shared<Settings<Date>>,
RelinkableHandle<dyn YieldTermStructure>,
Shared<OvernightIndex>,
Schedule,
) {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(today);
let curve: RelinkableHandle<dyn YieldTermStructure> = RelinkableHandle::empty();
let sofr = shared(Sofr::new(curve.handle(), settings.clone()));
let schedule = MakeSchedule::new()
.from(Date::new(1, Month::July, 2025))
.to(Date::new(1, Month::July, 2026))
.with_frequency(Frequency::Quarterly)
.with_calendar(UnitedStates::new(Market::GovernmentBond))
.with_convention(BusinessDayConvention::ModifiedFollowing)
.forwards()
.build();
(settings, curve, sofr, schedule)
}
fn flat_rate(reference: Date, rate: Rate) -> Shared<dyn YieldTermStructure> {
shared(FlatForward::with_rate(
reference,
rate,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>
}
fn base_leg(schedule: Schedule, sofr: Shared<OvernightIndex>) -> OvernightLeg {
OvernightLeg::new(schedule, sofr)
.with_notional(NOTIONAL)
.with_payment_day_counter(Actual360::new())
}
#[test]
fn a_quarterly_schedule_builds_four_compound_coupons() {
let (_settings, _curve, sofr, schedule) = common_vars(Date::new(1, Month::June, 2025));
let leg = base_leg(schedule, sofr);
assert_eq!(leg.build().unwrap().len(), 4);
let coupons = leg.coupons().unwrap();
assert_eq!(coupons.len(), 4);
for coupon in coupons {
assert_eq!(coupon.nominal(), NOTIONAL);
assert_eq!(coupon.averaging_method(), RateAveraging::Compound);
}
}
#[test]
fn gearings_and_spreads_broadcast_to_every_coupon() {
let (settings, curve, sofr, schedule) = common_vars(Date::new(1, Month::June, 2025));
curve.link_to(flat_rate(settings.evaluation_date().unwrap(), 0.0434));
let gearings = [1.0, 1.25, 2.0, 0.5];
let spreads = [0.0001, 0.0001, 0.0002, 0.0002];
let plain = base_leg(schedule.clone(), sofr.clone()).coupons().unwrap();
let geared = base_leg(schedule.clone(), sofr.clone())
.with_gearings(gearings.to_vec())
.coupons()
.unwrap();
let spreaded = base_leg(schedule, sofr)
.with_spreads(spreads.to_vec())
.coupons()
.unwrap();
assert_eq!(plain.len(), 4);
assert_eq!(geared.len(), 4);
assert_eq!(spreaded.len(), 4);
for i in 0..4 {
let plain_rate = plain[i].rate().unwrap();
assert!((geared[i].rate().unwrap() - gearings[i] * plain_rate).abs() < 1e-12);
assert!((spreaded[i].rate().unwrap() - (plain_rate + spreads[i])).abs() < 1e-12);
}
}
#[test]
fn invalid_legs_surface_errors_at_build() {
let (_settings, _curve, sofr, schedule) = common_vars(Date::new(1, Month::June, 2025));
assert!(
OvernightLeg::new(schedule.clone(), sofr.clone())
.build()
.is_err()
);
assert!(
base_leg(schedule.clone(), sofr.clone())
.with_averaging_method(RateAveraging::Simple)
.build()
.is_err()
);
assert!(base_leg(schedule, sofr).with_gearing(0.0).build().is_err());
}
}