use std::collections::HashMap;
#[cfg(test)]
use crate::ad::dual::DualFwd;
use crate::{
ad::scalar::Scalar,
core::{instrument::AssetClass, trade::Side},
currencies::currency::Currency,
indices::marketindex::MarketIndex,
instruments::cashflows::{
cashflow::SimpleCashflow, cashflowtype::CashflowType, fixedratecoupon::FixedRateCoupon,
floatingratecoupon::FloatingRateCoupon, leg::Leg,
optionembeddedcoupon::OptionEmbeddedCoupon, payoffops::PayoffOps,
},
rates::{compounding::Compounding, interestrate::InterestRate},
time::{
calendar::Calendar,
calendars::nullcalendar::NullCalendar,
date::Date,
enums::{BusinessDayConvention, DateGenerationRule, Frequency},
period::Period,
schedule::MakeSchedule,
},
utils::errors::{QSError, Result},
};
#[derive(Clone, Copy)]
pub enum RateType {
Fixed,
Floating,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum PaymentStructure {
Bullet,
EqualPayments,
EqualRedemptions,
Zero,
Other,
}
#[derive(Clone)]
pub struct MakeLeg<T: Scalar> {
leg_id: Option<usize>,
start_date: Option<Date>,
end_date: Option<Date>,
first_coupon_date: Option<Date>,
payment_frequency: Option<Frequency>,
tenor: Option<Period>,
currency: Option<Currency>,
side: Option<Side>,
notional: Option<f64>,
structure: Option<PaymentStructure>,
redemptions: Option<HashMap<Date, f64>>,
end_of_month: Option<bool>,
calendar: Option<Calendar>,
business_day_convention: Option<BusinessDayConvention>,
date_generation_rule: Option<DateGenerationRule>,
discount_index: Option<MarketIndex>,
rate_type: Option<RateType>,
spread: Option<f64>,
forward_index: Option<MarketIndex>,
rate: Option<InterestRate<T>>,
disbursements: Option<HashMap<Date, f64>>,
asset_class: AssetClass,
floorlet_strike: Option<f64>,
caplet_strike: Option<f64>,
}
impl<T> Default for MakeLeg<T>
where
T: Scalar,
{
fn default() -> Self {
Self {
leg_id: None,
start_date: None,
end_date: None,
first_coupon_date: None,
payment_frequency: None,
tenor: None,
currency: None,
side: None,
notional: None,
structure: None,
redemptions: None,
end_of_month: None,
calendar: None,
business_day_convention: None,
date_generation_rule: None,
rate_type: None,
spread: None,
discount_index: None,
forward_index: None,
rate: None,
disbursements: None,
asset_class: AssetClass::InterestRate,
floorlet_strike: None,
caplet_strike: None,
}
}
}
impl<T> MakeLeg<T>
where
T: Scalar,
{
#[must_use]
pub const fn with_asset_class(mut self, asset_class: AssetClass) -> Self {
self.asset_class = asset_class;
self
}
#[must_use]
pub const fn with_end_of_month(mut self, end_of_month: Option<bool>) -> Self {
self.end_of_month = end_of_month;
self
}
#[must_use]
pub const fn with_leg_id(mut self, leg_id: usize) -> Self {
self.leg_id = Some(leg_id);
self
}
#[must_use]
pub const fn with_first_coupon_date(mut self, first_coupon_date: Option<Date>) -> Self {
self.first_coupon_date = first_coupon_date;
self
}
#[must_use]
pub const fn with_floorlet_strike(mut self, floor: Option<f64>) -> Self {
self.floorlet_strike = floor;
self
}
#[must_use]
pub const fn with_caplet_strike(mut self, cap: Option<f64>) -> Self {
self.caplet_strike = cap;
self
}
#[must_use]
pub const fn with_rate_type(mut self, rate_type: RateType) -> Self {
self.rate_type = Some(rate_type);
self
}
#[must_use]
pub const fn with_currency(mut self, currency: Currency) -> Self {
self.currency = Some(currency);
self
}
#[must_use]
pub const fn with_side(mut self, side: Side) -> Self {
self.side = Some(side);
self
}
#[must_use]
pub const fn with_notional(mut self, notional: f64) -> Self {
self.notional = Some(notional);
self
}
#[must_use]
pub fn with_calendar(mut self, calendar: Option<Calendar>) -> Self {
self.calendar = calendar;
self
}
#[must_use]
pub const fn with_business_day_convention(
mut self,
business_day_convention: Option<BusinessDayConvention>,
) -> Self {
self.business_day_convention = business_day_convention;
self
}
#[must_use]
pub const fn with_date_generation_rule(
mut self,
date_generation_rule: Option<DateGenerationRule>,
) -> Self {
self.date_generation_rule = date_generation_rule;
self
}
#[must_use]
pub const fn with_start_date(mut self, start_date: Date) -> Self {
self.start_date = Some(start_date);
self
}
#[must_use]
pub const fn with_end_date(mut self, end_date: Date) -> Self {
self.end_date = Some(end_date);
self
}
#[must_use]
pub fn with_disbursements(mut self, disbursements: HashMap<Date, f64>) -> Self {
self.disbursements = Some(disbursements);
self
}
#[must_use]
pub fn with_redemptions(mut self, redemptions: HashMap<Date, f64>) -> Self {
self.redemptions = Some(redemptions);
self
}
#[must_use]
pub const fn with_rate(mut self, rate: InterestRate<T>) -> Self {
self.rate = Some(rate);
self
}
#[must_use]
pub const fn with_tenor(mut self, tenor: Period) -> Self {
self.tenor = Some(tenor);
self
}
#[must_use]
pub const fn with_payment_frequency(mut self, frequency: Frequency) -> Self {
self.payment_frequency = Some(frequency);
self
}
#[must_use]
pub fn with_discount_index(mut self, market_index: Option<MarketIndex>) -> Self {
self.discount_index = market_index;
self
}
#[must_use]
pub fn with_forward_index(mut self, market_index: MarketIndex) -> Self {
self.forward_index = Some(market_index);
self
}
#[must_use]
pub const fn with_spread(mut self, spread: f64) -> Self {
self.spread = Some(spread);
self
}
#[must_use]
pub const fn bullet(mut self) -> Self {
self.structure = Some(PaymentStructure::Bullet);
self
}
#[must_use]
pub const fn equal_redemptions(mut self) -> Self {
self.structure = Some(PaymentStructure::EqualRedemptions);
self
}
#[must_use]
pub const fn zero(mut self) -> Self {
self.structure = Some(PaymentStructure::Zero);
self.payment_frequency = Some(Frequency::Once);
self
}
#[must_use]
pub const fn equal_payments(mut self) -> Self {
self.structure = Some(PaymentStructure::EqualPayments);
self
}
#[must_use]
pub const fn other(mut self) -> Self {
self.structure = Some(PaymentStructure::Other);
self.payment_frequency = Some(Frequency::OtherFrequency);
self
}
#[must_use]
pub const fn with_structure(mut self, structure: PaymentStructure) -> Self {
self.structure = Some(structure);
self
}
}
enum LegType {
FixedRate,
FloatingRate,
OptionEmbedded,
}
impl<T> MakeLeg<T>
where
T: Scalar,
{
fn check_leg_type(&self) -> Result<LegType> {
match self.rate_type {
Some(RateType::Fixed) => {
if self.caplet_strike.is_some() || self.floorlet_strike.is_some() {
Err(QSError::InvalidValueErr(
"Caplet and floorlet strikes should not be set for fixed rate leg".into(),
))
} else {
Ok(LegType::FixedRate)
}
}
Some(RateType::Floating) => {
if self.structure == Some(PaymentStructure::EqualPayments) {
return Err(QSError::InvalidValueErr(
"Equal payments structure is not compatible with floating rate leg".into(),
));
}
if self.caplet_strike.is_some() || self.floorlet_strike.is_some() {
Ok(LegType::OptionEmbedded)
} else {
Ok(LegType::FloatingRate)
}
}
None => Err(QSError::ValueNotSetErr("Rate type".into())),
}
}
#[allow(clippy::too_many_lines)]
pub fn build(self) -> Result<Leg<T>> {
let mut cashflows = Vec::new();
let structure = self
.structure
.ok_or_else(|| QSError::ValueNotSetErr("Structure".into()))?;
let payment_frequency = self
.payment_frequency
.ok_or_else(|| QSError::ValueNotSetErr("Payment frequency".into()))?;
let side = self
.side
.ok_or_else(|| QSError::ValueNotSetErr("Side".into()))?;
let currency = self
.currency
.ok_or_else(|| QSError::ValueNotSetErr("Currency".into()))?;
let leg_id = self.leg_id.unwrap_or(0);
match structure {
PaymentStructure::Bullet => {
let leg_type = self.check_leg_type()?;
let start_date = self
.start_date
.ok_or_else(|| QSError::ValueNotSetErr("Start date".into()))?;
let end_date = if let Some(date) = self.end_date {
date
} else {
let tenor = self
.tenor
.ok_or_else(|| QSError::ValueNotSetErr("Tenor".into()))?;
start_date + tenor
};
let schedule_builder = MakeSchedule::new(start_date, end_date)
.with_frequency(payment_frequency)
.end_of_month(self.end_of_month.unwrap_or(false))
.with_calendar(
self.calendar
.unwrap_or(Calendar::NullCalendar(NullCalendar::new())),
)
.with_convention(
self.business_day_convention
.unwrap_or(BusinessDayConvention::Unadjusted),
)
.with_rule(
self.date_generation_rule
.unwrap_or(DateGenerationRule::Forward),
);
let schedule = if let Some(date) = self.first_coupon_date {
if date > start_date {
schedule_builder.with_first_date(date).build()?
} else {
Err(QSError::InvalidValueErr(
"First coupon date must be after start date".into(),
))?
}
} else {
schedule_builder.build()?
};
let notional = self
.notional
.ok_or_else(|| QSError::ValueNotSetErr("Notional".into()))?;
let side = self
.side
.ok_or_else(|| QSError::ValueNotSetErr("Side".into()))?;
let first_date = vec![*schedule
.dates()
.first()
.ok_or_else(|| QSError::ValueNotSetErr("Schedule dates".into()))?];
let last_date = vec![*schedule
.dates()
.last()
.ok_or_else(|| QSError::ValueNotSetErr("Schedule dates".into()))?];
let notionals = notionals_vector(
schedule.dates().len() - 1,
notional,
PaymentStructure::Bullet,
);
add_cashflows_to_vec(&mut cashflows, &first_date, &[notional], 1);
add_cashflows_to_vec(&mut cashflows, &last_date, &[notional], 0);
match leg_type {
LegType::FixedRate => {
let rate = self
.rate
.ok_or_else(|| QSError::ValueNotSetErr("Rate".into()))?;
build_fixed_rate_coupons_from_notionals(
&mut cashflows,
schedule.dates(),
¬ionals,
rate,
)?;
let leg = Leg::new(
leg_id,
cashflows,
currency,
self.discount_index,
None,
None,
self.rate,
side,
true,
self.asset_class,
schedule.dates()[1],
last_date[0],
);
Ok(leg)
}
LegType::FloatingRate => {
let spread = self
.spread
.ok_or_else(|| QSError::ValueNotSetErr("Spread".into()))?;
let forward_index = self.forward_index.ok_or_else(|| {
QSError::ValueNotSetErr("Forward Market Index".into())
})?;
build_floating_rate_coupons_from_notionals(
&mut cashflows,
schedule.dates(),
¬ionals,
T::scalar(spread),
&forward_index,
)?;
let leg = Leg::new(
leg_id,
cashflows,
currency,
self.discount_index,
Some(forward_index),
Some(T::scalar(spread)),
None,
side,
true,
self.asset_class,
schedule.dates()[1],
last_date[0],
);
Ok(leg)
}
LegType::OptionEmbedded => {
let spread = self
.spread
.ok_or_else(|| QSError::ValueNotSetErr("Spread".into()))?;
let forward_index = self.forward_index.ok_or_else(|| {
QSError::ValueNotSetErr("Forward Market Index".into())
})?;
build_embedded_option_coupons_from_notionals(
&mut cashflows,
schedule.dates(),
¬ionals,
T::scalar(spread),
&forward_index,
self.floorlet_strike,
self.caplet_strike,
)?;
let leg = Leg::new(
leg_id,
cashflows,
currency,
self.discount_index,
Some(forward_index),
Some(T::scalar(spread)),
None,
side,
true,
self.asset_class,
schedule.dates()[1],
last_date[0],
);
Ok(leg)
}
}
}
PaymentStructure::Other => {
let disbursements = self
.disbursements
.ok_or_else(|| QSError::ValueNotSetErr("Disbursements".into()))?;
let redemptions = self
.redemptions
.ok_or_else(|| QSError::ValueNotSetErr("Redemptions".into()))?;
let notional = disbursements.values().fold(0.0, |acc, x| acc + x).abs();
let redemption = redemptions.values().fold(0.0, |acc, x| acc + x).abs();
if (notional - redemption).abs() > 0.000_001 {
return Err(QSError::InvalidValueErr(
"Notional and redemption must be equal".into(),
));
}
for (date, amount) in &disbursements {
cashflows.push(CashflowType::Disbursement(SimpleCashflow::new(
*amount, *date,
)));
}
for (date, amount) in &redemptions {
cashflows.push(CashflowType::Redemption(SimpleCashflow::new(
*amount, *date,
)));
}
let mut all_dates: Vec<Date> = disbursements
.keys()
.chain(redemptions.keys())
.copied()
.collect();
all_dates.sort();
let first_pay = all_dates
.first()
.copied()
.unwrap_or_else(|| Date::new(1970, 1, 1));
let last_pay = all_dates
.last()
.copied()
.unwrap_or_else(|| Date::new(1970, 1, 1));
let leg = Leg::new(
leg_id,
cashflows,
currency,
self.discount_index,
None,
None,
None,
side,
true,
self.asset_class,
first_pay,
last_pay,
);
Ok(leg)
}
PaymentStructure::EqualPayments => {
let leg_type = self.check_leg_type()?;
let start_date = self
.start_date
.ok_or_else(|| QSError::ValueNotSetErr("Start date".into()))?;
let end_date = if let Some(date) = self.end_date {
date
} else {
let tenor = self
.tenor
.ok_or_else(|| QSError::ValueNotSetErr("Tenor".into()))?;
start_date + tenor
};
let schedule_builder = MakeSchedule::new(start_date, end_date)
.with_frequency(payment_frequency)
.end_of_month(self.end_of_month.unwrap_or(false))
.with_calendar(
self.calendar
.unwrap_or(Calendar::NullCalendar(NullCalendar::new())),
)
.with_convention(
self.business_day_convention
.unwrap_or(BusinessDayConvention::Unadjusted),
)
.with_rule(
self.date_generation_rule
.unwrap_or(DateGenerationRule::Backward),
);
let schedule = if let Some(date) = self.first_coupon_date {
if date > start_date {
schedule_builder.with_first_date(date).build()?
} else {
Err(QSError::InvalidValueErr(
"First coupon date must be after start date".into(),
))?
}
} else {
schedule_builder.build()?
};
let notional = self
.notional
.ok_or_else(|| QSError::ValueNotSetErr("Notional".into()))?;
let first_date = vec![*schedule
.dates()
.first()
.ok_or_else(|| QSError::ValueNotSetErr("Schedule dates".into()))?];
add_cashflows_to_vec(&mut cashflows, &first_date, &[notional], 1);
match leg_type {
LegType::FixedRate => {
let rate = self
.rate
.ok_or_else(|| QSError::ValueNotSetErr("Rate".into()))?;
let redemptions =
calculate_equal_payment_redemptions(schedule.dates(), rate, notional);
let mut notionals =
redemptions.iter().try_fold(vec![notional], |mut acc, x| {
let last = *acc.last().ok_or_else(|| {
QSError::InvalidValueErr(
"Notional schedule cannot be empty".into(),
)
})?;
acc.push(last - x);
Ok::<_, QSError>(acc)
})?;
notionals.pop();
build_fixed_rate_coupons_from_notionals(
&mut cashflows,
schedule.dates(),
¬ionals,
rate,
)?;
let redemption_dates: Vec<Date> =
schedule.dates().iter().skip(1).copied().collect();
add_cashflows_to_vec(&mut cashflows, &redemption_dates, &redemptions, 0);
let leg = Leg::new(
leg_id,
cashflows,
currency,
self.discount_index,
None,
None,
self.rate,
side,
true,
self.asset_class,
schedule.dates()[1],
*schedule
.dates()
.last()
.ok_or_else(|| QSError::InvalidValueErr("Empty schedule".into()))?,
);
Ok(leg)
}
LegType::FloatingRate | LegType::OptionEmbedded => {
Err(QSError::InvalidValueErr(
"EqualPayments structure is only supported for fixed rate legs".into(),
))
}
}
}
PaymentStructure::Zero => {
let start_date = self
.start_date
.ok_or_else(|| QSError::ValueNotSetErr("Start date".into()))?;
let end_date = if let Some(date) = self.end_date {
date
} else {
let tenor = self
.tenor
.ok_or_else(|| QSError::ValueNotSetErr("Tenor".into()))?;
start_date + tenor
};
let schedule = MakeSchedule::new(start_date, end_date)
.with_frequency(payment_frequency)
.with_convention(
self.business_day_convention
.unwrap_or(BusinessDayConvention::Unadjusted),
)
.with_calendar(
self.calendar
.unwrap_or(Calendar::NullCalendar(NullCalendar::new())),
)
.with_rule(
self.date_generation_rule
.unwrap_or(DateGenerationRule::Backward),
)
.build()?;
let notional = self
.notional
.ok_or_else(|| QSError::ValueNotSetErr("Notional".into()))?;
let first_date = vec![*schedule
.dates()
.first()
.ok_or_else(|| QSError::ValueNotSetErr("Schedule dates".into()))?];
let last_date = vec![*schedule
.dates()
.last()
.ok_or_else(|| QSError::ValueNotSetErr("Schedule dates".into()))?];
add_cashflows_to_vec(&mut cashflows, &first_date, &[notional], 1);
add_cashflows_to_vec(&mut cashflows, &last_date, &[notional], 0);
let leg = Leg::new(
leg_id,
cashflows,
currency,
self.discount_index,
None,
None,
None,
side,
true,
self.asset_class,
first_date[0],
last_date[0],
);
Ok(leg)
}
PaymentStructure::EqualRedemptions => {
let leg_type = self.check_leg_type()?;
let start_date = self
.start_date
.ok_or_else(|| QSError::ValueNotSetErr("Start date".into()))?;
let end_date = if let Some(date) = self.end_date {
date
} else {
let tenor = self
.tenor
.ok_or_else(|| QSError::ValueNotSetErr("Tenor".into()))?;
start_date + tenor
};
let schedule_builder = MakeSchedule::new(start_date, end_date)
.with_frequency(payment_frequency)
.end_of_month(self.end_of_month.unwrap_or(false))
.with_convention(
self.business_day_convention
.unwrap_or(BusinessDayConvention::Unadjusted),
)
.with_calendar(
self.calendar
.unwrap_or(Calendar::NullCalendar(NullCalendar::new())),
)
.with_rule(
self.date_generation_rule
.unwrap_or(DateGenerationRule::Backward),
);
let schedule = if let Some(date) = self.first_coupon_date {
if date > start_date {
schedule_builder.with_first_date(date).build()?
} else {
Err(QSError::InvalidValueErr(
"First coupon date must be after start date".into(),
))?
}
} else {
schedule_builder.build()?
};
let notional = self
.notional
.ok_or_else(|| QSError::ValueNotSetErr("Notional".into()))?;
let first_date = vec![*schedule
.dates()
.first()
.ok_or_else(|| QSError::ValueNotSetErr("Schedule dates".into()))?];
let n = schedule.dates().len() - 1;
let notionals = notionals_vector(n, notional, PaymentStructure::EqualRedemptions);
let n_f64 = f64::from(u32::try_from(n).map_err(|_| {
QSError::InvalidValueErr("Redemption count exceeds u32".into())
})?);
let redemptions = vec![notional / n_f64; n];
add_cashflows_to_vec(&mut cashflows, &first_date, &[notional], 1);
match leg_type {
LegType::FixedRate => {
let rate = self
.rate
.ok_or_else(|| QSError::ValueNotSetErr("Rate".into()))?;
build_fixed_rate_coupons_from_notionals(
&mut cashflows,
schedule.dates(),
¬ionals,
rate,
)?;
let redemption_dates: Vec<Date> =
schedule.dates().iter().skip(1).copied().collect();
add_cashflows_to_vec(&mut cashflows, &redemption_dates, &redemptions, 0);
let leg = Leg::new(
leg_id,
cashflows,
currency,
self.discount_index,
None,
None,
self.rate,
side,
true,
self.asset_class,
schedule.dates()[1],
*schedule
.dates()
.last()
.ok_or_else(|| QSError::InvalidValueErr("Empty schedule".into()))?,
);
Ok(leg)
}
LegType::FloatingRate => {
let spread = self
.spread
.ok_or_else(|| QSError::ValueNotSetErr("Spread".into()))?;
let forward_index = self.forward_index.ok_or_else(|| {
QSError::ValueNotSetErr("Forward Market Index".into())
})?;
build_floating_rate_coupons_from_notionals(
&mut cashflows,
schedule.dates(),
¬ionals,
T::scalar(spread),
&forward_index,
)?;
let redemption_dates: Vec<Date> =
schedule.dates().iter().skip(1).copied().collect();
add_cashflows_to_vec(&mut cashflows, &redemption_dates, &redemptions, 0);
let leg = Leg::new(
leg_id,
cashflows,
currency,
self.discount_index,
Some(forward_index),
Some(T::scalar(spread)),
None,
side,
true,
self.asset_class,
schedule.dates()[1],
*schedule
.dates()
.last()
.ok_or_else(|| QSError::InvalidValueErr("Empty schedule".into()))?,
);
Ok(leg)
}
LegType::OptionEmbedded => {
let spread = self
.spread
.ok_or_else(|| QSError::ValueNotSetErr("Spread".into()))?;
let forward_index = self.forward_index.ok_or_else(|| {
QSError::ValueNotSetErr("Forward Market Index".into())
})?;
build_embedded_option_coupons_from_notionals(
&mut cashflows,
schedule.dates(),
¬ionals,
T::scalar(spread),
&forward_index,
self.floorlet_strike,
self.caplet_strike,
)?;
let redemption_dates: Vec<Date> =
schedule.dates().iter().skip(1).copied().collect();
add_cashflows_to_vec(&mut cashflows, &redemption_dates, &redemptions, 0);
let leg = Leg::new(
leg_id,
cashflows,
currency,
self.discount_index,
Some(forward_index),
Some(T::scalar(spread)),
None,
side,
true,
self.asset_class,
schedule.dates()[1],
*schedule
.dates()
.last()
.ok_or_else(|| QSError::InvalidValueErr("Empty schedule".into()))?,
);
Ok(leg)
}
}
}
}
}
}
fn build_fixed_rate_coupons_from_notionals<T>(
cashflows: &mut Vec<CashflowType<T>>,
dates: &[Date],
notionals: &[f64],
rate: InterestRate<T>,
) -> Result<()>
where
T: Scalar,
{
if dates.len() - 1 != notionals.len() {
Err(QSError::InvalidValueErr(
"Dates and notionals must have the same length".to_string(),
))?;
}
if dates.len() < 2 {
Err(QSError::InvalidValueErr(
"Dates must have at least two elements".to_string(),
))?;
}
for (date_pair, notional) in dates.windows(2).zip(notionals) {
let d1 = date_pair[0];
let d2 = date_pair[1];
let coupon = FixedRateCoupon::new(*notional, Box::new(rate), d1, d2, d2);
cashflows.push(CashflowType::FixedRateCoupon(coupon));
}
Ok(())
}
fn build_floating_rate_coupons_from_notionals<T>(
cashflows: &mut Vec<CashflowType<T>>,
dates: &[Date],
notionals: &[f64],
spread: T,
market_index: &MarketIndex,
) -> Result<()>
where
T: Scalar,
{
if dates.len() - 1 != notionals.len() {
Err(QSError::InvalidValueErr(
"Dates and notionals must have the same length".to_string(),
))?;
}
if dates.len() < 2 {
Err(QSError::InvalidValueErr(
"Dates must have at least two elements".to_string(),
))?;
}
for (date_pair, notional) in dates.windows(2).zip(notionals) {
let d1 = date_pair[0];
let d2 = date_pair[1];
let coupon = FloatingRateCoupon::new(*notional, spread, market_index.clone(), d1, d2, d2);
cashflows.push(CashflowType::FloatingRateCoupon(coupon));
}
Ok(())
}
fn build_embedded_option_coupons_from_notionals<T>(
cashflows: &mut Vec<CashflowType<T>>,
dates: &[Date],
notionals: &[f64],
spread: T,
market_index: &MarketIndex,
floorlet_strike: Option<f64>,
caplet_strike: Option<f64>,
) -> Result<()>
where
T: Scalar,
{
if dates.len() - 1 != notionals.len() {
Err(QSError::InvalidValueErr(
"Dates and notionals must have the same length".to_string(),
))?;
}
if dates.len() < 2 {
Err(QSError::InvalidValueErr(
"Dates must have at least two elements".to_string(),
))?;
}
if floorlet_strike.is_none() && caplet_strike.is_none() {
Err(QSError::InvalidValueErr(
"At least one of floorlet or caplet strike must be set for option-embedded coupons"
.to_string(),
))?;
}
let payoff = match (floorlet_strike, caplet_strike) {
(Some(floor), Some(cap)) => PayoffOps::Max(
Box::new(PayoffOps::Min(
Box::new(PayoffOps::Index),
Box::new(PayoffOps::Const(floor)),
)),
Box::new(PayoffOps::Const(cap)),
),
(Some(floor), None) => PayoffOps::Min(
Box::new(PayoffOps::Index),
Box::new(PayoffOps::Const(floor)),
),
(None, Some(cap)) => {
PayoffOps::Max(Box::new(PayoffOps::Index), Box::new(PayoffOps::Const(cap)))
}
(None, None) => unreachable!(), };
for (date_pair, notional) in dates.windows(2).zip(notionals) {
let d1 = date_pair[0];
let d2 = date_pair[1];
let coupon = OptionEmbeddedCoupon::new(
*notional,
market_index.clone(),
spread,
d1,
d2,
d2,
payoff.clone(),
);
cashflows.push(CashflowType::OptionEmbeddedCoupon(coupon));
}
Ok(())
}
fn add_cashflows_to_vec<T>(
cashflows: &mut Vec<CashflowType<T>>,
dates: &[Date],
amounts: &[f64],
cashflow_type: usize,
) where
T: Scalar,
{
for (date, amount) in dates.iter().zip(amounts) {
let cashflow = SimpleCashflow::new(*amount, *date);
match cashflow_type {
0 => cashflows.push(CashflowType::Redemption(cashflow)),
1 => cashflows.push(CashflowType::Disbursement(cashflow)),
_ => (),
}
}
}
fn notionals_vector(n: usize, notional: f64, structure: PaymentStructure) -> Vec<f64> {
match structure {
PaymentStructure::Bullet => vec![notional; n],
PaymentStructure::EqualRedemptions => {
let redemptions = vec![
notional
/ f64::from(u32::try_from(n).unwrap_or_else(|_| {
panic!("notional schedule length should fit in u32")
}));
n
];
let mut results = Vec::new();
let mut sum = 0.0;
for r in redemptions {
results.push(notional - sum);
sum += r;
}
results
}
PaymentStructure::Zero => vec![notional; 1],
_ => vec![],
}
}
fn calculate_equal_payment_redemptions<T>(
dates: &[Date],
rate: InterestRate<T>,
notional: f64,
) -> Vec<f64>
where
T: Scalar,
{
let mut annuity_factor = 0.0;
for date_pair in dates.windows(2) {
let d1 = date_pair[0];
let d2 = date_pair[1];
let year_fraction = rate.day_counter().year_fraction(d1, d2);
let cf_f64 = compound_factor_from_yf_value(rate, year_fraction);
annuity_factor += 1.0 / cf_f64;
}
let payment = notional / annuity_factor;
let mut redemptions = Vec::new();
let mut balance = notional;
for date_pair in dates.windows(2) {
let d1 = date_pair[0];
let d2 = date_pair[1];
let year_fraction = rate.day_counter().year_fraction(d1, d2);
let cf_f64 = compound_factor_from_yf_value(rate, year_fraction);
let interest = balance * (cf_f64 - 1.0);
let principal = payment - interest;
balance -= principal;
redemptions.push(principal);
}
redemptions
}
fn compound_factor_from_yf_value<T>(rate: InterestRate<T>, year_fraction: f64) -> f64
where
T: Scalar,
{
let rate_value = rate.rate().value();
let f = f64::from(rate.frequency() as i32);
match rate.compounding() {
Compounding::Simple => rate_value.mul_add(year_fraction, 1.0),
Compounding::Compounded => (1.0 + rate_value / f).powf(f * year_fraction),
Compounding::Continuous => (rate_value * year_fraction).exp(),
Compounding::SimpleThenCompounded => {
if year_fraction <= 1.0 / f {
rate_value.mul_add(year_fraction, 1.0)
} else {
(1.0 + rate_value / f).powf(year_fraction * f)
}
}
Compounding::CompoundedThenSimple => {
if year_fraction > 1.0 / f {
rate_value.mul_add(year_fraction, 1.0)
} else {
(1.0 + rate_value / f).powf(year_fraction * f)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rates::compounding::Compounding;
use crate::rates::interestrate::RateDefinition;
use crate::time::daycounter::DayCounter;
fn create_test_leg_builder() -> MakeLeg<DualFwd> {
let start_date = Date::new(2024, 1, 1);
let end_date = Date::new(2025, 1, 1);
let rate = InterestRate::from_rate_definition(
DualFwd::new(0.05),
RateDefinition::new(
DayCounter::Actual360,
Compounding::Simple,
Frequency::Annual,
),
);
MakeLeg::<DualFwd>::default()
.with_start_date(start_date)
.with_end_date(end_date)
.with_notional(100_000.0)
.with_rate(rate)
.with_rate_type(RateType::Fixed)
.with_side(Side::PayShort)
.with_currency(Currency::USD)
.with_payment_frequency(Frequency::Annual)
.with_forward_index(MarketIndex::SOFR)
}
#[test]
fn test_bullet_fixed_rate_leg() {
let leg_builder = create_test_leg_builder().bullet();
let result = leg_builder.build();
if let Err(e) = &result {
panic!("Failed to build bullet leg: {}", e);
}
let leg = result.unwrap();
assert!(!leg.cashflows().is_empty(), "Leg should have cashflows");
}
#[test]
fn test_zero_coupon_leg() {
let leg_builder = create_test_leg_builder().zero();
let result = leg_builder.build();
assert!(result.is_ok(), "Failed to build zero coupon leg");
let leg = result.unwrap();
assert_eq!(
leg.cashflows().len(),
2,
"Zero coupon leg should have 2 cashflows (initial and final)"
);
}
#[test]
fn test_equal_redemptions_fixed_rate_leg() {
let leg_builder = create_test_leg_builder()
.equal_redemptions()
.with_payment_frequency(Frequency::Semiannual);
let result = leg_builder.build();
if let Err(e) = &result {
panic!("Failed to build equal redemptions leg: {}", e);
}
let leg = result.unwrap();
assert!(!leg.cashflows().is_empty(), "Leg should have cashflows");
}
#[test]
fn test_equal_payments_fixed_rate_leg() {
let leg_builder = create_test_leg_builder()
.equal_payments()
.with_payment_frequency(Frequency::Annual);
let result = leg_builder.build();
if let Err(e) = &result {
panic!("Failed to build equal payments leg: {}", e);
}
let leg = result.unwrap();
assert!(!leg.cashflows().is_empty(), "Leg should have cashflows");
}
#[test]
fn test_missing_notional_error() {
let start_date = Date::new(2024, 1, 1);
let end_date = Date::new(2025, 1, 1);
let leg_builder = MakeLeg::<DualFwd>::default()
.with_start_date(start_date)
.with_end_date(end_date)
.bullet()
.with_rate_type(RateType::Fixed)
.with_side(Side::PayShort)
.with_currency(Currency::USD)
.with_payment_frequency(Frequency::Annual);
let result = leg_builder.build();
assert!(result.is_err(), "Should fail when notional is not set");
}
#[test]
fn test_missing_rate_error_for_fixed_rate() {
let start_date = Date::new(2024, 1, 1);
let end_date = Date::new(2025, 1, 1);
let leg_builder = MakeLeg::<DualFwd>::default()
.with_start_date(start_date)
.with_end_date(end_date)
.with_notional(100_000.0)
.bullet()
.with_rate_type(RateType::Fixed)
.with_side(Side::PayShort)
.with_currency(Currency::USD)
.with_payment_frequency(Frequency::Annual);
let result = leg_builder.build();
assert!(
result.is_err(),
"Should fail when rate is not set for fixed rate leg"
);
}
#[test]
fn test_other_structure_with_disbursements_and_redemptions() {
let start_date = Date::new(2024, 1, 1);
let end_date = Date::new(2025, 1, 1);
let mut disbursements = HashMap::new();
disbursements.insert(start_date, 100_000.0);
let mut redemptions = HashMap::new();
redemptions.insert(end_date, 100_000.0);
let leg_builder = MakeLeg::<DualFwd>::default()
.with_start_date(start_date)
.with_end_date(end_date)
.with_notional(100_000.0)
.other()
.with_side(Side::PayShort)
.with_currency(Currency::USD)
.with_disbursements(disbursements)
.with_redemptions(redemptions);
let result = leg_builder.build();
if let Err(e) = &result {
panic!("Failed to build other structure leg: {}", e);
}
let leg = result.unwrap();
assert!(!leg.cashflows().is_empty(), "Leg should have cashflows");
}
#[test]
fn test_other_structure_unequal_notional_error() {
let start_date = Date::new(2024, 1, 1);
let end_date = Date::new(2025, 1, 1);
let mut disbursements = HashMap::new();
disbursements.insert(start_date, 100_000.0);
let mut redemptions = HashMap::new();
redemptions.insert(end_date, 95_000.0);
let leg_builder = MakeLeg::<DualFwd>::default()
.with_start_date(start_date)
.with_end_date(end_date)
.other()
.with_side(Side::PayShort)
.with_currency(Currency::USD)
.with_disbursements(disbursements)
.with_redemptions(redemptions);
let result = leg_builder.build();
assert!(
result.is_err(),
"Should fail when disbursements and redemptions are unequal"
);
}
}