Skip to content
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,12 @@ Quantrs supports options pricing with various models for both vanilla and exotic
- Bond Types
- [x] _Zero-Coupon Bonds_
- [ ] _Treasury Bonds_ (fixed-rate coupon)
- [ ] _Corporate Bonds_ (fixed-rate coupon with credit spreads)
- [x] _Corporate Bonds_ (fixed-rate coupon with credit spreads)
- [ ] _Floating-Rate Bonds_ (variable coupon with caps/floors)
- [ ] Duration (_Macaulay_, _Modified_, _Effective_)
- [ ] Convexity
- [ ] Yield Measures (_YTM_, _YTC_, _YTW_)
- [x] Day Count Conventions (_ACT/365F_, _ACT/365_, _ACT/360_, _30/360 US_, _30/360 Eurobond_, _ACT/ACT ISDA_, _ACT/ACT ICMA_)
- [x] Accrual Conventions (_ACT/365F_, _ACT/365_, _ACT/360_, _30/360 US_, _30/360 Eurobond_, _ACT/ACT ISDA_, _ACT/ACT ICMA_)

## Usage

Expand Down
4 changes: 2 additions & 2 deletions src/fixed_income/bonds.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
//! Module for various bond types.

// pub use corporate::CorporateBond;
pub use corporate::CorporateBond;
// pub use floating_rate::FloatingRateBond;
// pub use treasury::TreasuryBond;
pub use zero_coupon::ZeroCouponBond;

// mod corporate;
mod corporate;
// mod floating_rate;
// mod treasury;
mod zero_coupon;
120 changes: 120 additions & 0 deletions src/fixed_income/bonds/corporate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use crate::fixed_income::{Bond, BondPricingError, DayCount, PriceResult};
use chrono::{Datelike, NaiveDate};

#[derive(Debug, Clone)]
pub struct CorporateBond {
pub face_value: f64,
pub coupon_rate: f64,
pub maturity: NaiveDate,
pub frequency: u32,
pub credit_rating: String,
}

impl CorporateBond {
pub fn new(
face_value: f64,
coupon_rate: f64,
maturity: NaiveDate,
frequency: u32,
credit_rating: String,
) -> Self {
Self {
face_value,
coupon_rate,
maturity,
frequency,
credit_rating,
}
}

pub fn credit_spread(&self) -> f64 {
// Simple credit spread based on rating
// TODO: Replace (maybe)
match self.credit_rating.as_str() {
"AAA" => 0.005, // 50 bps
"AA" => 0.010, // 100 bps
"A" => 0.015, // 150 bps
"BBB" => 0.025, // 250 bps
"BB" => 0.050, // 500 bps
"B" => 0.100, // 1000 bps
_ => 0.030, // Default spread
}
}
}

impl Bond for CorporateBond {
fn price(
&self,
settlement: NaiveDate,
ytm: f64,
day_count: DayCount,
) -> Result<PriceResult, BondPricingError> {
if ytm < 0.0 {
return Err(BondPricingError::invalid_yield(ytm));
}

if settlement >= self.maturity {
return Err(BondPricingError::settlement_after_maturity(
settlement,
self.maturity,
));
}

if ![1, 2, 4, 12].contains(&self.frequency) {
return Err(BondPricingError::InvalidFrequency(self.frequency));
}

// Add credit spread to yield
let adjusted_ytm = ytm + self.credit_spread();

// Calculate time to maturity in years
let days_to_maturity = (self.maturity - settlement).num_days() as f64;
let years_to_maturity = match day_count {
DayCount::Act365F => days_to_maturity / 365.0,
DayCount::Act360 => days_to_maturity / 360.0,
DayCount::Thirty360US => {
let years = (self.maturity.year() - settlement.year()) as f64;
let months =
(self.maturity.month() as i32 - settlement.month() as i32) as f64 / 12.0;
let days = (self.maturity.day() as i32 - settlement.day() as i32) as f64 / 360.0;
years + months + days
}
_ => days_to_maturity / 365.0,
};

// Calculate periodic coupon payment
let coupon_payment = self.face_value * self.coupon_rate / self.frequency as f64;

// Calculate number of coupon payments
let num_payments = (years_to_maturity * self.frequency as f64).ceil() as u32;

// Calculate present value of coupon payments
let mut pv_coupons = 0.0;
let periodic_rate = adjusted_ytm / self.frequency as f64;

for i in 1..=num_payments {
let discount_factor = (1.0 + periodic_rate).powi(-(i as i32));
pv_coupons += coupon_payment * discount_factor;
}

// Calculate present value of principal
let pv_principal = self.face_value / (1.0 + periodic_rate).powi(num_payments as i32);

// Total clean price
let clean_price = pv_coupons + pv_principal;

// Calculate accrued interest
let accrued = self.accrued_interest(settlement, day_count);

// Dirty price = clean price + accrued interest
let dirty_price = clean_price + accrued;

Ok(PriceResult::new(clean_price, dirty_price, accrued))
}

fn accrued_interest(&self, settlement: NaiveDate, day_count: DayCount) -> f64 {
// TODO: Implement proper accrued interest based on day count convention
let coupon_payment = self.face_value * self.coupon_rate / self.frequency as f64;
coupon_payment * 0.5 // Placeholder
}
}
Loading