#[derive(Debug, Clone, PartialEq)]
pub enum DayCount {
Actual360,
Actual365,
Thirty360,
}
pub fn day_count_fraction(day_count: &DayCount, days: u32) -> f64 {
match day_count {
DayCount::Actual360 => days as f64 / 360.0,
DayCount::Actual365 => days as f64 / 365.0,
DayCount::Thirty360 => days as f64 / 360.0,
}
}
#[derive(Debug, Clone)]
pub struct SwapLeg {
pub notional: f64,
pub fixed_rate: Option<f64>,
pub floating_spread: f64,
pub payment_frequency: u32,
pub day_count: DayCount,
}
#[derive(Debug, Clone)]
pub struct SwapSpec {
pub notional: f64,
pub fixed_rate: f64,
pub tenor_years: f64,
pub payment_frequency: u32,
pub day_count: DayCount,
}
#[derive(Debug, Clone)]
pub struct DiscountCurve {
pub tenors: Vec<f64>,
pub zero_rates: Vec<f64>,
}
impl DiscountCurve {
pub fn discount_factor(&self, t: f64) -> f64 {
if t <= 0.0 {
return 1.0;
}
let r = self.interpolate_rate(t);
(-r * t).exp()
}
fn interpolate_rate(&self, t: f64) -> f64 {
let n = self.tenors.len();
if n == 0 {
return 0.0;
}
if t <= self.tenors[0] {
return self.zero_rates[0];
}
if t >= self.tenors[n - 1] {
return self.zero_rates[n - 1];
}
for i in 0..n - 1 {
let t0 = self.tenors[i];
let t1 = self.tenors[i + 1];
if t >= t0 && t <= t1 {
let alpha = (t - t0) / (t1 - t0);
return self.zero_rates[i] * (1.0 - alpha) + self.zero_rates[i + 1] * alpha;
}
}
self.zero_rates[n - 1]
}
pub fn from_par_rates(tenors: &[f64], par_rates: &[f64]) -> Self {
assert_eq!(tenors.len(), par_rates.len(), "tenors and par_rates must have equal length");
let n = tenors.len();
let mut dfs: Vec<f64> = Vec::with_capacity(n);
let mut zero_rates: Vec<f64> = Vec::with_capacity(n);
for i in 0..n {
let t = tenors[i];
let c = par_rates[i];
let mut pv_coupons = 0.0;
let steps = i; for j in 0..steps {
let t_prev = if j == 0 { 0.0 } else { tenors[j - 1] };
let t_j = tenors[j];
let dcf = t_j - t_prev;
pv_coupons += c * dcf * dfs[j];
}
let t_prev_last = if i == 0 { 0.0 } else { tenors[i - 1] };
let dcf_last = t - t_prev_last;
let df_i = (1.0 - pv_coupons) / (1.0 + c * dcf_last);
dfs.push(df_i);
let r = if t > 0.0 { -df_i.ln() / t } else { 0.0 };
zero_rates.push(r);
}
DiscountCurve {
tenors: tenors.to_vec(),
zero_rates,
}
}
}
pub fn fixed_leg_pv(spec: &SwapSpec, curve: &DiscountCurve) -> f64 {
let n_payments = (spec.tenor_years * spec.payment_frequency as f64).round() as u32;
let dt = 1.0 / spec.payment_frequency as f64;
let days_per_period = (365.0 * dt).round() as u32;
let dcf = day_count_fraction(&spec.day_count, days_per_period);
let mut pv = 0.0;
for k in 1..=n_payments {
let t = k as f64 * dt;
let df = curve.discount_factor(t);
pv += spec.notional * spec.fixed_rate * dcf * df;
}
pv += spec.notional * curve.discount_factor(spec.tenor_years);
pv
}
pub fn floating_leg_pv(spec: &SwapSpec, curve: &DiscountCurve) -> f64 {
let df_t = curve.discount_factor(spec.tenor_years);
let n_payments = (spec.tenor_years * spec.payment_frequency as f64).round() as u32;
let dt = 1.0 / spec.payment_frequency as f64;
let days_per_period = (365.0 * dt).round() as u32;
let dcf = day_count_fraction(&spec.day_count, days_per_period);
let mut pv = 0.0;
let mut df_prev = 1.0;
for k in 1..=n_payments {
let t = k as f64 * dt;
let df = curve.discount_factor(t);
let fwd = (df_prev / df - 1.0) / dcf;
pv += spec.notional * fwd * dcf * df;
df_prev = df;
}
pv += spec.notional * df_t;
pv
}
pub fn swap_npv(spec: &SwapSpec, curve: &DiscountCurve, receive_fixed: bool) -> f64 {
let fixed_pv = fixed_leg_pv(spec, curve);
let float_pv = floating_leg_pv(spec, curve);
if receive_fixed {
fixed_pv - float_pv
} else {
float_pv - fixed_pv
}
}
pub fn par_swap_rate(spec: &SwapSpec, curve: &DiscountCurve) -> f64 {
let n_payments = (spec.tenor_years * spec.payment_frequency as f64).round() as u32;
let dt = 1.0 / spec.payment_frequency as f64;
let days_per_period = (365.0 * dt).round() as u32;
let dcf = day_count_fraction(&spec.day_count, days_per_period);
let df_t = curve.discount_factor(spec.tenor_years);
let mut annuity = 0.0;
for k in 1..=n_payments {
let t = k as f64 * dt;
annuity += curve.discount_factor(t) * dcf;
}
(1.0 - df_t) / annuity
}
pub fn swap_dv01(spec: &SwapSpec, curve: &DiscountCurve, receive_fixed: bool) -> f64 {
let bump = 0.0001;
let bumped_rates: Vec<f64> = curve.zero_rates.iter().map(|r| r + bump).collect();
let bumped_curve = DiscountCurve {
tenors: curve.tenors.clone(),
zero_rates: bumped_rates,
};
let npv_base = swap_npv(spec, curve, receive_fixed);
let npv_bumped = swap_npv(spec, &bumped_curve, receive_fixed);
npv_bumped - npv_base
}
pub fn swap_duration(spec: &SwapSpec, curve: &DiscountCurve) -> f64 {
let n_payments = (spec.tenor_years * spec.payment_frequency as f64).round() as u32;
let dt = 1.0 / spec.payment_frequency as f64;
let days_per_period = (365.0 * dt).round() as u32;
let dcf = day_count_fraction(&spec.day_count, days_per_period);
let mut weighted_sum = 0.0;
let mut pv_sum = 0.0;
for k in 1..=n_payments {
let t = k as f64 * dt;
let df = curve.discount_factor(t);
let cf = spec.notional * spec.fixed_rate * dcf;
weighted_sum += t * cf * df;
pv_sum += cf * df;
}
let df_t = curve.discount_factor(spec.tenor_years);
weighted_sum += spec.tenor_years * spec.notional * df_t;
pv_sum += spec.notional * df_t;
if pv_sum == 0.0 {
return 0.0;
}
let mac_duration = weighted_sum / pv_sum;
let r = curve.interpolate_rate(spec.tenor_years);
mac_duration / (1.0 + r / spec.payment_frequency as f64)
}
#[derive(Debug, Clone)]
pub struct SwapValuation {
pub npv: f64,
pub fixed_pv: f64,
pub floating_pv: f64,
pub par_rate: f64,
pub dv01: f64,
pub duration: f64,
}
pub fn value_swap(spec: &SwapSpec, curve: &DiscountCurve, receive_fixed: bool) -> SwapValuation {
let fixed_pv = fixed_leg_pv(spec, curve);
let floating_pv = floating_leg_pv(spec, curve);
let npv = if receive_fixed {
fixed_pv - floating_pv
} else {
floating_pv - fixed_pv
};
SwapValuation {
npv,
fixed_pv,
floating_pv,
par_rate: par_swap_rate(spec, curve),
dv01: swap_dv01(spec, curve, receive_fixed),
duration: swap_duration(spec, curve),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn flat_curve(rate: f64) -> DiscountCurve {
DiscountCurve {
tenors: vec![0.5, 1.0, 2.0, 3.0, 5.0, 7.0, 10.0],
zero_rates: vec![rate; 7],
}
}
#[test]
fn test_swap_npv_at_par_rate_is_zero() {
let curve = flat_curve(0.05);
let par = par_swap_rate(
&SwapSpec {
notional: 1_000_000.0,
fixed_rate: 0.05,
tenor_years: 5.0,
payment_frequency: 2,
day_count: DayCount::Actual365,
},
&curve,
);
let spec = SwapSpec {
notional: 1_000_000.0,
fixed_rate: par,
tenor_years: 5.0,
payment_frequency: 2,
day_count: DayCount::Actual365,
};
let npv = swap_npv(&spec, &curve, true);
assert!(npv.abs() < 1.0, "NPV at par rate should be near zero, got {}", npv);
}
#[test]
fn test_dv01_positive_for_receiver() {
let curve = flat_curve(0.05);
let spec = SwapSpec {
notional: 1_000_000.0,
fixed_rate: 0.05,
tenor_years: 5.0,
payment_frequency: 2,
day_count: DayCount::Actual365,
};
let dv01 = swap_dv01(&spec, &curve, true);
assert!(dv01.abs() > 0.0);
}
#[test]
fn test_day_count_fraction() {
assert!((day_count_fraction(&DayCount::Actual360, 180) - 0.5).abs() < 1e-10);
assert!((day_count_fraction(&DayCount::Actual365, 365) - 1.0).abs() < 1e-10);
assert!((day_count_fraction(&DayCount::Thirty360, 90) - 0.25).abs() < 1e-10);
}
#[test]
fn test_bootstrap_consistent_discount_factors() {
let tenors = vec![1.0, 2.0, 3.0, 5.0];
let par_rates = vec![0.04, 0.045, 0.048, 0.052];
let curve = DiscountCurve::from_par_rates(&tenors, &par_rates);
for (i, &t) in tenors.iter().enumerate() {
let spec = SwapSpec {
notional: 1.0,
fixed_rate: par_rates[i],
tenor_years: t,
payment_frequency: 1,
day_count: DayCount::Actual365,
};
let npv = swap_npv(&spec, &curve, true);
assert!(
npv.abs() < 1e-6,
"Bootstrap should give zero NPV for tenor={}, got {}",
t,
npv
);
}
}
#[test]
fn test_fixed_leg_pv_high_rate() {
let curve = flat_curve(0.01);
let spec = SwapSpec {
notional: 1_000_000.0,
fixed_rate: 0.20,
tenor_years: 2.0,
payment_frequency: 1,
day_count: DayCount::Actual365,
};
let pv = fixed_leg_pv(&spec, &curve);
assert!(pv > spec.notional, "Fixed leg PV should exceed notional at very high coupon");
}
}