use crate::cashflow::{CashFlow, Leg};
use crate::cashflows::{Coupon, OvernightIndexedCoupon, OvernightLeg, RateAveraging};
use crate::errors::QlResult;
use crate::indexes::OvernightIndex;
use crate::indexes::interestrateindex::InterestRateIndex;
use crate::instrument::{Instrument, InstrumentBase};
use crate::instruments::fixedvsfloatingswap::{
FixedVsFloatingSwap, FixedVsFloatingSwapArguments, FloatingArgumentsFn,
};
use crate::instruments::swap::SwapType;
use crate::pricingengine::{Arguments, Results};
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::schedule::Schedule;
use crate::types::{Integer, Rate, Real, Spread};
pub struct OvernightIndexedSwap {
base: FixedVsFloatingSwap,
overnight_index: Shared<OvernightIndex>,
payment_lag: Integer,
payment_calendar: Calendar,
averaging_method: RateAveraging,
}
impl OvernightIndexedSwap {
#[allow(clippy::too_many_arguments)]
pub fn with_nominal(
swap_type: SwapType,
nominal: Real,
fixed_schedule: Schedule,
fixed_rate: Rate,
fixed_day_count: DayCounter,
overnight_schedule: Schedule,
overnight_index: Shared<OvernightIndex>,
spread: Spread,
payment_lag: Integer,
payment_adjustment: BusinessDayConvention,
payment_calendar: Option<Calendar>,
averaging_method: RateAveraging,
settings: Shared<Settings<Date>>,
) -> QlResult<OvernightIndexedSwap> {
OvernightIndexedSwap::new(
swap_type,
vec![nominal],
fixed_schedule,
fixed_rate,
fixed_day_count,
vec![nominal],
overnight_schedule,
overnight_index,
spread,
payment_lag,
payment_adjustment,
payment_calendar,
averaging_method,
settings,
)
}
#[allow(clippy::too_many_arguments)]
pub fn new(
swap_type: SwapType,
fixed_nominals: Vec<Real>,
fixed_schedule: Schedule,
fixed_rate: Rate,
fixed_day_count: DayCounter,
overnight_nominals: Vec<Real>,
overnight_schedule: Schedule,
overnight_index: Shared<OvernightIndex>,
spread: Spread,
payment_lag: Integer,
payment_adjustment: BusinessDayConvention,
payment_calendar: Option<Calendar>,
averaging_method: RateAveraging,
settings: Shared<Settings<Date>>,
) -> QlResult<OvernightIndexedSwap> {
let resolved_calendar =
payment_calendar.unwrap_or_else(|| overnight_schedule.calendar().clone());
let coupons =
OvernightLeg::new(overnight_schedule.clone(), Shared::clone(&overnight_index))
.with_notionals(overnight_nominals.clone())
.with_spread(spread)
.with_payment_lag(payment_lag)
.with_payment_adjustment(payment_adjustment)
.with_payment_calendar(resolved_calendar.clone())
.with_averaging_method(averaging_method)
.coupons()?;
let floating_leg: Leg = coupons
.iter()
.map(|coupon| Shared::clone(coupon) as Shared<dyn CashFlow>)
.collect();
let floating_arguments: FloatingArgumentsFn =
Box::new(move |_swap, args| fill_floating_arguments(&coupons, args));
let base = FixedVsFloatingSwap::new(
swap_type,
fixed_nominals,
fixed_schedule,
fixed_rate,
Some(fixed_day_count),
overnight_nominals,
overnight_schedule,
overnight_index.ibor_index(),
spread,
overnight_index.day_counter().clone(),
None,
payment_lag,
Some(resolved_calendar.clone()),
floating_leg,
floating_arguments,
settings,
)?;
Ok(OvernightIndexedSwap {
base,
overnight_index,
payment_lag,
payment_calendar: resolved_calendar,
averaging_method,
})
}
pub fn fixed_vs_floating(&self) -> &FixedVsFloatingSwap {
&self.base
}
pub fn fixed_vs_floating_mut(&mut self) -> &mut FixedVsFloatingSwap {
&mut self.base
}
pub fn into_fixed_vs_floating(self) -> FixedVsFloatingSwap {
self.base
}
pub fn overnight_index(&self) -> &Shared<OvernightIndex> {
&self.overnight_index
}
pub fn overnight_nominals(&self) -> &[Real] {
self.base.floating_nominals()
}
pub fn overnight_schedule(&self) -> &Schedule {
self.base.floating_schedule()
}
pub fn overnight_leg(&self) -> &Leg {
self.base.floating_leg()
}
pub fn spread(&self) -> Spread {
self.base.spread()
}
pub fn payment_frequency(&self) -> Frequency {
let fixed = self.base.fixed_schedule().tenor().frequency();
let floating = self.base.floating_schedule().tenor().frequency();
if floating as i16 > fixed as i16 {
floating
} else {
fixed
}
}
pub fn payment_lag(&self) -> Integer {
self.payment_lag
}
pub fn payment_calendar(&self) -> &Calendar {
&self.payment_calendar
}
pub fn averaging_method(&self) -> RateAveraging {
self.averaging_method
}
pub fn overnight_leg_bps(&mut self) -> QlResult<Real> {
self.base.floating_leg_bps()
}
pub fn overnight_leg_npv(&mut self) -> QlResult<Real> {
self.base.floating_leg_npv()
}
}
fn fill_floating_arguments(
coupons: &[Shared<OvernightIndexedCoupon>],
args: &mut FixedVsFloatingSwapArguments,
) -> QlResult<()> {
let n = coupons.len();
args.floating_reset_dates = Vec::with_capacity(n);
args.floating_pay_dates = Vec::with_capacity(n);
args.floating_nominals = Vec::with_capacity(n);
args.floating_fixing_dates = Vec::with_capacity(n);
args.floating_accrual_times = Vec::with_capacity(n);
args.floating_spreads = Vec::with_capacity(n);
args.floating_coupons = Vec::with_capacity(n);
for coupon in coupons {
args.floating_reset_dates.push(coupon.accrual_start_date());
args.floating_pay_dates
.push(coupon.coupon_base().payment_date());
args.floating_nominals.push(coupon.nominal());
args.floating_fixing_dates.push(coupon.fixing_date());
args.floating_accrual_times.push(coupon.accrual_period());
args.floating_spreads.push(coupon.spread());
args.floating_coupons.push(Coupon::amount(coupon.as_ref())?);
}
Ok(())
}
impl Instrument for OvernightIndexedSwap {
fn base(&self) -> &InstrumentBase {
self.base.base()
}
fn base_mut(&mut self) -> &mut InstrumentBase {
self.base.base_mut()
}
fn is_expired(&self) -> QlResult<bool> {
self.base.is_expired()
}
fn setup_expired(&mut self) {
self.base.setup_expired();
}
fn setup_arguments(&self, arguments: &mut dyn Arguments) -> QlResult<()> {
self.base.setup_arguments(arguments)
}
fn fetch_results(&mut self, results: &dyn Results) -> QlResult<()> {
self.base.fetch_results(results)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::handle::Handle;
use crate::indexes::ibor::Sofr;
use crate::instruments::SwapArguments;
use crate::interestrate::Compounding;
use crate::pricingengine::PricingEngine;
use crate::pricingengines::DiscountingSwapEngine;
use crate::shared::{SharedMut, shared, shared_mut};
use crate::termstructures::yields::FlatForward;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::calendars::unitedstates::{Market, UnitedStates};
use crate::time::date::Month;
use crate::time::daycounters::actual360::Actual360;
use crate::time::schedule::MakeSchedule;
const NOMINAL: Real = 1_000_000.0;
const FIXED_RATE: Rate = 0.03;
const SPREAD: Spread = 0.001;
fn today() -> Date {
Date::new(1, Month::June, 2025)
}
fn settings_today() -> Shared<Settings<Date>> {
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(today());
settings
}
fn flat_curve() -> Handle<dyn YieldTermStructure> {
Handle::new(shared(FlatForward::with_rate(
today(),
0.02,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>)
}
fn sofr(settings: &Shared<Settings<Date>>) -> Shared<OvernightIndex> {
shared(Sofr::new(flat_curve(), Shared::clone(settings)))
}
fn fixed_schedule() -> Schedule {
MakeSchedule::new()
.from(Date::new(1, Month::July, 2025))
.to(Date::new(1, Month::July, 2027))
.with_frequency(Frequency::Annual)
.with_calendar(UnitedStates::new(Market::GovernmentBond))
.with_convention(BusinessDayConvention::ModifiedFollowing)
.forwards()
.build()
}
fn overnight_schedule() -> Schedule {
MakeSchedule::new()
.from(Date::new(1, Month::July, 2025))
.to(Date::new(1, Month::July, 2027))
.with_frequency(Frequency::Quarterly)
.with_calendar(UnitedStates::new(Market::GovernmentBond))
.with_convention(BusinessDayConvention::ModifiedFollowing)
.forwards()
.build()
}
fn make_swap(swap_type: SwapType) -> OvernightIndexedSwap {
let settings = settings_today();
let index = sofr(&settings);
OvernightIndexedSwap::with_nominal(
swap_type,
NOMINAL,
fixed_schedule(),
FIXED_RATE,
Actual360::new(),
overnight_schedule(),
index,
SPREAD,
0,
BusinessDayConvention::Following,
None,
RateAveraging::Compound,
settings,
)
.unwrap()
}
#[test]
fn it_builds_both_legs_and_exposes_the_base() {
let swap = make_swap(SwapType::Payer);
assert_eq!(swap.fixed_vs_floating().fixed_leg().len(), 2);
assert_eq!(swap.overnight_leg().len(), 8);
assert_eq!(swap.fixed_vs_floating().swap_type(), SwapType::Payer);
assert_eq!(swap.fixed_vs_floating().fixed_rate(), FIXED_RATE);
assert_eq!(swap.spread(), SPREAD);
assert_eq!(swap.fixed_vs_floating().nominal().unwrap(), NOMINAL);
assert_eq!(swap.overnight_nominals(), [NOMINAL]);
assert_eq!(
swap.overnight_schedule().calendar().name(),
UnitedStates::new(Market::GovernmentBond).name(),
"the overnight schedule the swap was built over"
);
assert_eq!(swap.payment_lag(), 0);
assert_eq!(swap.averaging_method(), RateAveraging::Compound);
assert_eq!(swap.payment_frequency(), Frequency::Quarterly);
assert_eq!(
swap.payment_calendar().name(),
overnight_schedule().calendar().name(),
"empty payment calendar falls back to the overnight schedule's"
);
}
#[test]
fn payer_type_maps_the_leg_multipliers_through_delegation() {
let mut payer = SwapArguments::default();
make_swap(SwapType::Payer)
.setup_arguments(&mut payer)
.unwrap();
assert_eq!(payer.payer, vec![-1.0, 1.0], "payer: -fixed, +overnight");
let mut receiver = SwapArguments::default();
make_swap(SwapType::Receiver)
.setup_arguments(&mut receiver)
.unwrap();
assert_eq!(
receiver.payer,
vec![1.0, -1.0],
"receiver: +fixed, -overnight"
);
}
#[test]
fn the_floating_hook_fills_the_argument_vectors() {
let swap = make_swap(SwapType::Payer);
let mut args = FixedVsFloatingSwapArguments::default();
swap.setup_arguments(&mut args).unwrap();
let leg = swap.overnight_leg();
let n = leg.len();
assert_eq!(n, 8);
let coupons: Vec<&dyn Coupon> = leg.iter().map(|f| f.as_coupon().unwrap()).collect();
let expected_resets: Vec<Date> = coupons.iter().map(|c| c.accrual_start_date()).collect();
assert_eq!(args.floating_reset_dates, expected_resets);
let expected_pays: Vec<Date> = leg.iter().map(|f| f.date()).collect();
assert_eq!(args.floating_pay_dates, expected_pays);
assert_eq!(args.floating_nominals, vec![NOMINAL; n]);
let expected_accruals: Vec<_> = coupons.iter().map(|c| c.accrual_period()).collect();
assert_eq!(args.floating_accrual_times, expected_accruals);
assert_eq!(
args.floating_spreads,
vec![SPREAD; n],
"one spread per coupon"
);
let expected_amounts: Vec<Real> = leg.iter().map(|f| f.amount().unwrap()).collect();
assert_eq!(args.floating_coupons, expected_amounts);
assert_eq!(args.floating_fixing_dates.len(), n);
for (fixing, reset) in args.floating_fixing_dates.iter().zip(&expected_resets) {
assert!(fixing > reset, "overnight coupon fixes in arrears");
}
}
#[test]
fn fair_values_are_unavailable_before_pricing() {
let mut swap = make_swap(SwapType::Payer);
assert!(swap.fixed_vs_floating_mut().fair_rate().is_err());
assert!(swap.fixed_vs_floating_mut().fair_spread().is_err());
}
#[test]
fn it_prices_through_the_discounting_engine() {
let mut swap = make_swap(SwapType::Payer);
let engine = shared_mut(DiscountingSwapEngine::new(
flat_curve(),
None,
None,
None,
settings_today(),
));
swap.base_mut()
.set_pricing_engine(engine as SharedMut<dyn PricingEngine>);
let npv = swap.npv().unwrap();
assert!(npv.is_finite(), "swap NPV is finite");
assert!(
swap.overnight_leg_npv().unwrap().is_finite(),
"overnight-leg NPV is finite"
);
assert!(
swap.fixed_vs_floating_mut().fair_rate().unwrap() > 0.0,
"a fair rate is available once priced"
);
}
}