From 48d4d72537e21d7000003b767570803ada7e1061 Mon Sep 17 00:00:00 2001 From: carlobortolan Date: Tue, 19 Aug 2025 02:21:23 +0200 Subject: [PATCH 1/6] Add FI module definition --- src/fixed_income/bonds.rs | 11 +++++++++++ src/fixed_income/cashflow.rs | 22 +++++++++++++++++++++ src/fixed_income/day_count.rs | 36 +++++++++++++++++++++++++++++++++++ tests/fixed_income.rs | 30 +++++++++++++++++++++++++++++ 4 files changed, 99 insertions(+) create mode 100644 src/fixed_income/bonds.rs create mode 100644 src/fixed_income/cashflow.rs create mode 100644 src/fixed_income/day_count.rs create mode 100644 tests/fixed_income.rs diff --git a/src/fixed_income/bonds.rs b/src/fixed_income/bonds.rs new file mode 100644 index 0000000..b6cd19e --- /dev/null +++ b/src/fixed_income/bonds.rs @@ -0,0 +1,11 @@ +//! Module for various bond . + +pub use corporate::CorporateBond; +pub use floating_rate::FloatingRateBond; +pub use treasury::TreasuryBond; +pub use zero_coupon::ZeroCouponBond; + +mod corporate; +mod floating_rate; +mod treasury; +mod zero_coupon; diff --git a/src/fixed_income/cashflow.rs b/src/fixed_income/cashflow.rs new file mode 100644 index 0000000..b41f4f5 --- /dev/null +++ b/src/fixed_income/cashflow.rs @@ -0,0 +1,22 @@ +use chrono::NaiveDate; + +use crate::fixed_income::CashFlowType; + +/// Generate coupon dates from maturity backwards given months per period. +/// EOM handling and stubs to be implemented later. +pub fn generate_schedule( + maturity: NaiveDate, + settlement: NaiveDate, + period_months: i32, +) -> Vec { + // TODO: implement properly + vec![maturity] // placeholder +} + +#[derive(Debug, Clone)] +pub struct CashFlow { + pub date: NaiveDate, + pub amount: f64, + pub currency: Option, // Make optional + pub flow_type: CashFlowType, // Add type classification +} diff --git a/src/fixed_income/day_count.rs b/src/fixed_income/day_count.rs new file mode 100644 index 0000000..fbcfce2 --- /dev/null +++ b/src/fixed_income/day_count.rs @@ -0,0 +1,36 @@ +use chrono::NaiveDate; + +use crate::fixed_income::DayCount; + +pub fn year_fraction(start: NaiveDate, end: NaiveDate, dc: DayCount) -> f64 { + match dc { + DayCount::Act365F => { + let days = (end - start).num_days().max(0) as f64; + days / 365.0 + } + DayCount::Thirty360US => { + // TODO: implement NASD 30/360 + unimplemented!() + } + DayCount::ActActISDA => { + // TODO: implement Actual/Actual ISDA + unimplemented!() + } + DayCount::Act360 => { + // TODO: implement Actual/360 + unimplemented!() + } + DayCount::Thirty360E => { + // TODO: implement 30/360 European + unimplemented!() + } + DayCount::Act365 => { + // TODO: implement Actual/365 + unimplemented!() + } + DayCount::ActActICMA => { + // TODO: implement Actual/Actual ICMA + unimplemented!() + } + } +} diff --git a/tests/fixed_income.rs b/tests/fixed_income.rs new file mode 100644 index 0000000..e0cf605 --- /dev/null +++ b/tests/fixed_income.rs @@ -0,0 +1,30 @@ +#[cfg(test)] +mod tests { + mod corporate_bond_tests { + #[test] + fn test_corporate_bond_price() { + assert!(true, "Corporate bond pricing test not implemented yet"); + } + } + + mod floating_rate_bond_tests { + #[test] + fn test_floating_rate_bond_price() { + assert!(true, "Floating rate bond pricing test not implemented yet"); + } + } + + mod treasury_bond_tests { + #[test] + fn test_treasury_bond_price() { + assert!(true, "Treasury bond pricing test not implemented yet"); + } + } + + mod zero_coupon_bond_tests { + #[test] + fn test_zero_coupon_bond_price() { + assert!(true, "Zero coupon bond pricing test not implemented yet"); + } + } +} From ed94353a35d8b5ff1806f66b987acc006fd8bf8e Mon Sep 17 00:00:00 2001 From: carlobortolan Date: Tue, 19 Aug 2025 02:54:18 +0200 Subject: [PATCH 2/6] Add trait module for FI --- src/fixed_income.rs | 32 ++++++++++++++++++++++++++++ src/fixed_income/traits/bond.rs | 13 +++++++++++ src/fixed_income/traits/cashflow.rs | 13 +++++++++++ src/fixed_income/traits/day_count.rs | 6 ++++++ src/lib.rs | 1 + 5 files changed, 65 insertions(+) create mode 100644 src/fixed_income.rs create mode 100644 src/fixed_income/traits/bond.rs create mode 100644 src/fixed_income/traits/cashflow.rs create mode 100644 src/fixed_income/traits/day_count.rs diff --git a/src/fixed_income.rs b/src/fixed_income.rs new file mode 100644 index 0000000..d98666e --- /dev/null +++ b/src/fixed_income.rs @@ -0,0 +1,32 @@ +//! Module for fixed income securities +//! +//! Provides types and traits for various fixed income instruments, including bonds, cash flows, and day count conventions. +//! +//! This module includes: +//! +//! - **Bond Pricing**: Functions for calculating the present value of bonds, including yield to maturity and duration. +//! - **Bonds**: Definitions for different types of bonds +//! - **Cash Flow**: Structures and methods for handling cash flows associated with fixed income securities. +//! - **Day Count Conventions**: Implementations of various day count conventions used in fixed income calculations. +//! - **Types**: Additional types for specialized fixed income instruments. +//! +//! ## Supported instruments +//! +//! - [Treasury Bonds](bonds/struct.TreasuryBond.html) +//! - [Corporate Bonds](bonds/struct.CorporateBond.html) +//! - [Floating Rate Bonds](bonds/struct.FloatingRateBond.html) +//! - [Zero-Coupon Bonds](bonds/struct.ZeroCouponBond.html) + +pub use self::bond_pricing::*; +pub use self::bonds::*; +pub use self::cashflow::*; +pub use self::day_count::*; +pub use self::types::*; +pub use traits::*; + +mod bond_pricing; +mod bonds; +mod cashflow; +mod day_count; +mod traits; +mod types; diff --git a/src/fixed_income/traits/bond.rs b/src/fixed_income/traits/bond.rs new file mode 100644 index 0000000..669f89f --- /dev/null +++ b/src/fixed_income/traits/bond.rs @@ -0,0 +1,13 @@ +use crate::fixed_income::{BondPricingError, DayCount, PriceResult}; +use chrono::NaiveDate; + +pub trait Bond { + fn price( + &self, + settlement: NaiveDate, + ytm: f64, + day_count: DayCount, + ) -> Result; + + fn accrued_interest(&self, settlement: NaiveDate, day_count: DayCount) -> f64; +} diff --git a/src/fixed_income/traits/cashflow.rs b/src/fixed_income/traits/cashflow.rs new file mode 100644 index 0000000..35ca45a --- /dev/null +++ b/src/fixed_income/traits/cashflow.rs @@ -0,0 +1,13 @@ +use chrono::NaiveDate; + +use crate::fixed_income::CashFlow; + +pub trait CashFlowGenerator { + fn generate_cash_flows(&self) -> Vec; + fn cash_flows_between(&self, start: NaiveDate, end: NaiveDate) -> Vec; +} +pub trait CashFlowAnalysis { + fn present_value(&self, cash_flows: &[CashFlow], discount_rate: f64) -> f64; + fn total_cash_flows(&self, cash_flows: &[CashFlow]) -> f64; + fn cash_flow_summary(&self, cash_flows: &[CashFlow]) -> String; +} diff --git a/src/fixed_income/traits/day_count.rs b/src/fixed_income/traits/day_count.rs new file mode 100644 index 0000000..742885d --- /dev/null +++ b/src/fixed_income/traits/day_count.rs @@ -0,0 +1,6 @@ +use chrono::NaiveDate; + +pub trait DayCountConvention { + fn year_fraction(&self, start: NaiveDate, end: NaiveDate) -> f64; + fn day_count(&self, start: NaiveDate, end: NaiveDate) -> i32; +} diff --git a/src/lib.rs b/src/lib.rs index 4f96622..1856af6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,4 +39,5 @@ mod macros { } pub mod data; +pub mod fixed_income; pub mod options; From fb1b1ac9f0fd564f1aa2207259c4739933c3b11e Mon Sep 17 00:00:00 2001 From: carlobortolan Date: Tue, 19 Aug 2025 03:12:19 +0200 Subject: [PATCH 3/6] Add PriceResult struct --- src/fixed_income/bond_pricing.rs | 35 ++++++++++++++++++++++++++++++++ src/fixed_income/traits.rs | 9 ++++++++ 2 files changed, 44 insertions(+) create mode 100644 src/fixed_income/bond_pricing.rs create mode 100644 src/fixed_income/traits.rs diff --git a/src/fixed_income/bond_pricing.rs b/src/fixed_income/bond_pricing.rs new file mode 100644 index 0000000..abfb84b --- /dev/null +++ b/src/fixed_income/bond_pricing.rs @@ -0,0 +1,35 @@ +use std::fmt::Error; + +use chrono::NaiveDate; + +use crate::fixed_income::DayCount; + +#[derive(Debug, Clone, Copy)] +pub struct PriceResult { + pub clean: f64, + pub dirty: f64, + pub accrued: f64, +} + +pub fn bond_price( + face: f64, + coupon_rate: f64, + ytm: f64, + settlement: NaiveDate, + maturity: NaiveDate, + freq: u32, + day_count: DayCount, +) -> Result { + // TODO: implement pricing + unimplemented!() +} + +impl PriceResult { + pub fn new(clean: f64, dirty: f64, accrued: f64) -> Self { + Self { + clean, + dirty, + accrued, + } + } +} diff --git a/src/fixed_income/traits.rs b/src/fixed_income/traits.rs new file mode 100644 index 0000000..d07501a --- /dev/null +++ b/src/fixed_income/traits.rs @@ -0,0 +1,9 @@ +//! Module for various bond traits. + +pub use bond::Bond; +pub use cashflow::{CashFlowAnalysis, CashFlowGenerator}; +pub use day_count::DayCountConvention; + +mod bond; +mod cashflow; +mod day_count; From 5d0c5bbaf1fd51b31ac19aac955c5327ab1bea41 Mon Sep 17 00:00:00 2001 From: carlobortolan Date: Tue, 19 Aug 2025 03:33:04 +0200 Subject: [PATCH 4/6] Add FI types and custom pricing errors --- src/fixed_income/types.rs | 123 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 src/fixed_income/types.rs diff --git a/src/fixed_income/types.rs b/src/fixed_income/types.rs new file mode 100644 index 0000000..f298eee --- /dev/null +++ b/src/fixed_income/types.rs @@ -0,0 +1,123 @@ +//! Module for fixed income types and error handling +//!! ## References +//! - [Wikipedia: Day Count Convention](https://en.wikipedia.org/wiki/Day_count_convention) +//! - [Wikipedia: Cash Flow](https://en.wikipedia.org/wiki/Cash_flow) + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DayCount { + /// Actual/365 Fixed - 365 days per year + Act365F, + /// 30/360 US (Bond Basis) - 30 days per month, 360 days per year + Thirty360US, + /// Actual/Actual ISDA - actual days, actual year length + ActActISDA, + /// Actual/360 - actual days, 360 days per year + Act360, + /// 30/360 European - European version of 30/360 + Thirty360E, + /// Actual/365 - actual days, 365 days per year (no leap year adjustment) + Act365, + /// Actual/Actual ICMA - used for bonds + ActActICMA, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CashFlowType { + Coupon, + Principal, + CallPayment, + Other, +} + +#[derive(Debug, Clone)] +pub enum BondPricingError { + /// Invalid yield to maturity (e.g., negative or extremely high values) + InvalidYield(f64), + + /// Settlement date is after maturity date + SettlementAfterMaturity { + settlement: chrono::NaiveDate, + maturity: chrono::NaiveDate, + }, + + /// Invalid coupon frequency (must be 1, 2, 4, or 12) + InvalidFrequency(u32), + + /// Negative face value or coupon rate + NegativeInput(String), + + /// Error in payment schedule generation + ScheduleGenerationError(String), + + /// Mathematical calculation error (e.g., division by zero) + CalculationError(String), + + /// Invalid day count convention + InvalidDayCount, + + /// Missing required bond parameters + MissingParameter(String), +} + +impl std::fmt::Display for BondPricingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BondPricingError::InvalidYield(ytm) => { + write!(f, "Invalid yield to maturity: {ytm}") + } + BondPricingError::SettlementAfterMaturity { + settlement, + maturity, + } => { + write!( + f, + "Settlement date ({settlement}) must be before maturity date ({maturity})" + ) + } + BondPricingError::InvalidFrequency(freq) => { + write!( + f, + "Invalid coupon frequency: {freq}. Must be 1, 2, 4, or 12" + ) + } + BondPricingError::NegativeInput(param) => { + write!(f, "Negative input not allowed for: {param}") + } + BondPricingError::ScheduleGenerationError(msg) => { + write!(f, "Schedule generation error: {msg}") + } + BondPricingError::CalculationError(msg) => { + write!(f, "Calculation error: {msg}") + } + BondPricingError::InvalidDayCount => { + write!(f, "Invalid day count convention") + } + BondPricingError::MissingParameter(param) => { + write!(f, "Missing required parameter: {param}") + } + } + } +} + +impl std::error::Error for BondPricingError {} + +// Helper methods for creating common errors +impl BondPricingError { + pub fn invalid_yield(ytm: f64) -> Self { + Self::InvalidYield(ytm) + } + + pub fn settlement_after_maturity( + settlement: chrono::NaiveDate, + maturity: chrono::NaiveDate, + ) -> Self { + Self::SettlementAfterMaturity { + settlement, + maturity, + } + } + + pub fn negative_input(param: &str) -> Self { + Self::NegativeInput(param.to_string()) + } +} From 91de0e9f067edeef6b52a8b9bbfaa9e848f9c991 Mon Sep 17 00:00:00 2001 From: carlobortolan Date: Sun, 31 Aug 2025 23:33:41 +0200 Subject: [PATCH 5/6] update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a1ab809..d6f97da 100644 --- a/README.md +++ b/README.md @@ -91,12 +91,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 From d340b221ec3d17093e0e02ce1b92f960868c77cb Mon Sep 17 00:00:00 2001 From: carlobortolan Date: Sun, 31 Aug 2025 23:35:24 +0200 Subject: [PATCH 6/6] implement basic CorporateBond struct and pricing logic --- src/fixed_income/bonds/corporate.rs | 120 ++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 src/fixed_income/bonds/corporate.rs diff --git a/src/fixed_income/bonds/corporate.rs b/src/fixed_income/bonds/corporate.rs new file mode 100644 index 0000000..3665780 --- /dev/null +++ b/src/fixed_income/bonds/corporate.rs @@ -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 { + 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 + } +}