use crate::{
cashflows::{
cashflow::{Cashflow, CashflowType, Side},
simplecashflow::SimpleCashflow,
},
currencies::enums::Currency,
time::date::Date,
utils::errors::{AtlasError, Result},
};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::hash::BuildHasher;
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Structure {
Bullet,
EqualRedemptions,
Zero,
EqualPayments,
Other,
}
impl TryFrom<String> for Structure {
type Error = AtlasError;
fn try_from(s: String) -> Result<Self> {
match s.as_str() {
"Bullet" => Ok(Self::Bullet),
"EqualRedemptions" => Ok(Self::EqualRedemptions),
"Zero" => Ok(Self::Zero),
"EqualPayments" => Ok(Self::EqualPayments),
"Other" => Ok(Self::Other),
_ => Err(AtlasError::InvalidValueErr(format!(
"Invalid structure: {s}"
))),
}
}
}
impl From<Structure> for String {
fn from(structure: Structure) -> Self {
match structure {
Structure::Bullet => "Bullet".to_string(),
Structure::EqualRedemptions => "EqualRedemptions".to_string(),
Structure::Zero => "Zero".to_string(),
Structure::EqualPayments => "EqualPayments".to_string(),
Structure::Other => "Other".to_string(),
}
}
}
impl TryFrom<String> for CashflowType {
type Error = AtlasError;
fn try_from(s: String) -> Result<Self> {
match s.as_str() {
"Redemption" => Ok(Self::Redemption),
"Disbursement" => Ok(Self::Disbursement),
"FixedRateCoupon" => Ok(Self::FixedRateCoupon),
"FloatingRateCoupon" => Ok(Self::FloatingRateCoupon),
_ => Err(AtlasError::InvalidValueErr(format!(
"Invalid cashflow type: {s}"
))),
}
}
}
impl From<CashflowType> for String {
fn from(cashflow_type: CashflowType) -> Self {
match cashflow_type {
CashflowType::Redemption => "Redemption".to_string(),
CashflowType::Disbursement => "Disbursement".to_string(),
CashflowType::FixedRateCoupon => "FixedRateCoupon".to_string(),
CashflowType::FloatingRateCoupon => "FloatingRateCoupon".to_string(),
}
}
}
#[must_use]
pub fn infer_cashflows_from_amounts(
dates: &[Date],
amounts: &[f64],
side: Side,
currency: Currency,
) -> Vec<Cashflow> {
let mut cashflows = Vec::new();
dates.iter().zip(amounts).for_each(|(date, amount)| {
if *amount < 0.0 {
let cashflow =
SimpleCashflow::new(*date, currency, side.inverse()).with_amount(amount.abs());
match side.inverse() {
Side::Receive => cashflows.push(Cashflow::Redemption(cashflow)),
Side::Pay => cashflows.push(Cashflow::Disbursement(cashflow)),
}
} else {
let cashflow = SimpleCashflow::new(*date, currency, side).with_amount(*amount);
match side {
Side::Receive => cashflows.push(Cashflow::Redemption(cashflow)),
Side::Pay => cashflows.push(Cashflow::Disbursement(cashflow)),
}
}
});
cashflows
}
pub fn add_cashflows_to_vec(
cashflows: &mut Vec<Cashflow>,
dates: &[Date],
amounts: &[f64],
side: Side,
currency: Currency,
cashflow_type: CashflowType,
) {
for (date, amount) in dates.iter().zip(amounts) {
let cashflow = SimpleCashflow::new(*date, currency, side).with_amount(*amount);
match cashflow_type {
CashflowType::Redemption => cashflows.push(Cashflow::Redemption(cashflow)),
CashflowType::Disbursement => cashflows.push(Cashflow::Disbursement(cashflow)),
_ => (),
}
}
}
#[must_use]
pub fn notionals_vector(n: usize, notional: f64, structure: Structure) -> Vec<f64> {
match structure {
Structure::Bullet => vec![notional; n],
Structure::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
}
Structure::Zero => vec![notional; 1],
_ => vec![],
}
}
#[must_use]
pub fn calculate_outstanding(
disbursements: &HashMap<Date, f64, impl BuildHasher>,
redemptions: &HashMap<Date, f64, impl BuildHasher>,
additional_dates: &HashSet<Date, impl BuildHasher>,
) -> Vec<(Date, Date, f64)> {
let mut outstanding = Vec::new();
let mut timeline: Vec<(Date, f64)> = disbursements.iter().map(|(k, v)| (*k, *v)).collect();
for (date, amount) in redemptions {
match timeline.iter_mut().find(|(d, _)| *d == *date) {
Some((_, a)) => *a -= amount,
None => timeline.push((*date, -amount)),
}
}
for date in additional_dates {
if timeline.iter().all(|(d, _)| d != date) {
timeline.push((*date, 0.0));
}
}
timeline.sort_by_key(|k| k.0);
let mut event_iter = timeline.iter();
let (mut period_start, mut current_amount) = match event_iter.next() {
Some((date, amount)) => (*date, *amount),
None => return Vec::new(),
};
for (date, amount) in event_iter {
let period_end = *date;
outstanding.push((period_start, period_end, current_amount));
current_amount += amount;
period_start = period_end;
}
outstanding
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::{HashMap, HashSet};
#[test]
fn basic_test_cases() {
let mut redemptions = HashMap::new();
redemptions.insert(Date::new(2023, 8, 27), 50.0);
redemptions.insert(Date::new(2023, 9, 27), 50.0);
redemptions.insert(Date::new(2023, 10, 27), 50.0);
let mut disbursements = HashMap::new();
disbursements.insert(Date::new(2023, 6, 27), 150.0);
let result = calculate_outstanding(&disbursements, &redemptions, &HashSet::new());
assert_eq!(result.len(), 3);
assert_eq!(
result[0],
(Date::new(2023, 6, 27), Date::new(2023, 8, 27), 150.0)
);
assert_eq!(
result[1],
(Date::new(2023, 8, 27), Date::new(2023, 9, 27), 100.0)
);
assert_eq!(
result[2],
(Date::new(2023, 9, 27), Date::new(2023, 10, 27), 50.0)
);
}
#[test]
fn additional_dates() {
let mut redemptions = HashMap::new();
redemptions.insert(Date::new(2023, 8, 27), 50.0);
redemptions.insert(Date::new(2023, 9, 27), 50.0);
redemptions.insert(Date::new(2023, 11, 27), 50.0);
let mut disbursements = HashMap::new();
disbursements.insert(Date::new(2023, 6, 27), 150.0);
let mut additional_dates = HashSet::new();
additional_dates.insert(Date::new(2023, 10, 27));
let result = calculate_outstanding(&disbursements, &redemptions, &additional_dates);
assert_eq!(result.len(), 4);
assert_eq!(
result[0],
(Date::new(2023, 6, 27), Date::new(2023, 8, 27), 150.0)
);
assert_eq!(
result[1],
(Date::new(2023, 8, 27), Date::new(2023, 9, 27), 100.0)
);
assert_eq!(
result[2],
(Date::new(2023, 9, 27), Date::new(2023, 10, 27), 50.0)
);
assert_eq!(
result[3],
(Date::new(2023, 10, 27), Date::new(2023, 11, 27), 50.0)
);
}
#[test]
fn test_add_cashflows_to_vec() {
let mut cashflows = Vec::new();
let dates = vec![Date::new(2023, 8, 27), Date::new(2023, 9, 27)];
let amounts = vec![50.0, 50.0];
add_cashflows_to_vec(
&mut cashflows,
&dates,
&amounts,
Side::Receive,
Currency::USD,
CashflowType::Redemption,
);
assert_eq!(cashflows.len(), 2);
assert_eq!(
cashflows[0],
Cashflow::Redemption(
SimpleCashflow::new(Date::new(2023, 8, 27), Currency::USD, Side::Receive)
.with_amount(50.0)
)
);
assert_eq!(
cashflows[1],
Cashflow::Redemption(
SimpleCashflow::new(Date::new(2023, 9, 27), Currency::USD, Side::Receive,)
.with_amount(50.0)
)
);
}
}