Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
48d4d72
Add FI module definition
carlobortolan Aug 19, 2025
ed94353
Add trait module for FI
carlobortolan Aug 19, 2025
fb1b1ac
Add PriceResult struct
carlobortolan Aug 19, 2025
5d0c5bb
Add FI types and custom pricing errors
carlobortolan Aug 19, 2025
aa8f815
Add ZeroCouponBond struct and implement Bond trait for pricing
carlobortolan Aug 19, 2025
9e72238
Update day_count to include year_fraction and day_count methods
carlobortolan Aug 19, 2025
2f8a9b2
Add TODOs
carlobortolan Aug 19, 2025
c01e75c
Merge branch '7-implement-bond-pricing' of github.com:carlobortolan/q…
carlobortolan Aug 19, 2025
3e5c4df
Refactor ZeroCouponBond pricing logic to use DayCountConvention
carlobortolan Aug 19, 2025
54977cc
Refactor bond module documentation and add test cases for ZeroCouponBond
carlobortolan Aug 19, 2025
726e543
Remove out unused future bond imports
carlobortolan Aug 19, 2025
bc40eb1
Add basic tests
carlobortolan Aug 19, 2025
bddbf3b
Add cashflow tests for schedule generation and price result validation
carlobortolan Aug 19, 2025
9c4c1e1
Merge branch '7-implement-bond-pricing' of github.com:carlobortolan/q…
carlobortolan Aug 19, 2025
5ae6580
Remove unused imports and bond_price function stub from bond_pricing.rs
carlobortolan Aug 19, 2025
5d525f1
Merge branch '7-implement-bond-pricing' of github.com:carlobortolan/q…
carlobortolan Aug 19, 2025
799f460
Update README and add Zero Coupon Bond example
carlobortolan Aug 30, 2025
cb5b2be
Merge branch 'master' of github.com:carlobortolan/quantrs into 73-imp…
carlobortolan Aug 30, 2025
32aed25
update zero coupon bond tests and add day count tests
carlobortolan Aug 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

Quantrs is a tiny quantitative finance library for Rust.
It is designed to be as intuitive and easy to use as possible so that you can work with derivatives without the need to write complex code or have a PhD in reading QuantLib documentation.
The library is still in the early stages of development, and many features are not yet implemented.
The library is still in the early stages of development and many features are not yet implemented.

Please check out the documentation [here][docs-url].

Expand Down Expand Up @@ -86,6 +86,18 @@ Quantrs supports options pricing with various models for both vanilla and exotic

</details>

### Fixed Income

- Bond Types
- [x] _Zero-Coupon Bonds_
- [ ] _Treasury Bonds_ (fixed-rate coupon)
- [ ] _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_)

## Usage

Add this to your `Cargo.toml`:
Expand Down
22 changes: 22 additions & 0 deletions examples/fixed_income.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use quantrs::fixed_income::{Bond, DayCount, ZeroCouponBond};

fn main() {
let face_value = 1000.0;
let maturity = chrono::NaiveDate::from_ymd_opt(2030, 1, 1).unwrap_or_default();
let settlement = chrono::NaiveDate::from_ymd_opt(2025, 1, 1).unwrap_or_default();
let ytm = 0.05; // 5% yield to maturity
let day_count = DayCount::ActActICMA;

let zero_coupon_bond = ZeroCouponBond::new(face_value, maturity);

match zero_coupon_bond.price(settlement, ytm, day_count) {
Ok(price_result) => {
println!("Clean Price: {:.2}", price_result.clean);
println!("Dirty Price: {:.2}", price_result.dirty);
println!("Accrued Interest: {:.2}", price_result.accrued);
}
Err(e) => {
eprintln!("Error pricing bond: {}", e);
}
}
}
1 change: 1 addition & 0 deletions src/fixed_income.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
//! - [Zero-Coupon Bonds](bonds/struct.ZeroCouponBond.html)

pub use self::bond_pricing::*;
pub use self::bonds::*;
pub use self::cashflow::*;
pub use self::types::*;
pub use traits::*;
Expand Down
6 changes: 3 additions & 3 deletions src/fixed_income/bonds.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
//! Module for various bond .
//! Module for various bond types.

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

// mod corporate;
// mod floating_rate;
// mod treasury;
// mod zero_coupon;
mod zero_coupon;
83 changes: 83 additions & 0 deletions src/fixed_income/bonds/zero_coupon.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/// Zero Coupon Bond implementation
///
/// Example:
///
/// use quantrs::fixed_income::{Bond, DayCount, ZeroCouponBond};
/// fn main() {
/// let face_value = 1000.0;
/// let maturity = chrono::NaiveDate::from_ymd_opt(2030, 1, 1).unwrap_or_default();
/// let settlement = chrono::NaiveDate::from_ymd_opt(2025, 1, 1).unwrap_or_default();
/// let ytm = 0.05; // 5% yield to maturity
/// let day_count = DayCount::ActActICMA;
/// let zero_coupon_bond = ZeroCouponBond::new(face_value, maturity);
/// match zero_coupon_bond.price(settlement, ytm, day_count) {
/// Ok(price_result) => {
/// println!("Clean Price: {:.2}", price_result.clean);
/// println!("Dirty Price: {:.2}", price_result.dirty);
/// println!("Accrued Interest: {:.2}", price_result.accrued);
/// }
/// Err(e) => {
/// eprintln!("Error pricing bond: {}", e);
/// }
/// }
/// }
///
/// Note: Zero coupon bonds do not have accrued interest.
///
/// # References
/// - Fabozzi, Frank J. "Bond Markets, Analysis and Strategies." 9th Edition. Pearson, 2013.
/// - https://dqydj.com/zero-coupon-bond-calculator
use crate::fixed_income::{Bond, BondPricingError, DayCount, PriceResult};
use chrono::NaiveDate;

#[derive(Debug, Clone)]
pub struct ZeroCouponBond {
pub face_value: f64,
pub maturity: NaiveDate,
}

impl ZeroCouponBond {
pub fn new(face_value: f64, maturity: NaiveDate) -> Self {
Self {
face_value,
maturity,
}
}
}

impl Bond for ZeroCouponBond {
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,
));
}

let years_to_maturity = crate::fixed_income::DayCountConvention::year_fraction(
&day_count,
settlement,
self.maturity,
);

let clean_price = self.face_value / (1.0 + ytm).powf(years_to_maturity);
let accrued = self.accrued_interest(settlement, day_count);
let dirty_price = clean_price;

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

fn accrued_interest(&self, _settlement: NaiveDate, _day_count: DayCount) -> f64 {
// Zero coupon bonds have no accrued interest
0.0
}
}
29 changes: 20 additions & 9 deletions src/fixed_income/day_count.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
/// Implementations of various day count conventions for fixed income calculations.
///
/// References:
/// - https://www.isda.org/2011/01/07/act-act-icma
/// - https://www.isda.org/a/NIJEE/ICMA-Rule-Book-Rule-251-reproduced-by-permission-of-ICMA.pdf
/// - https://quant.stackexchange.com/questions/71858
/// - https://www.investopedia.com/terms/d/daycountconvention.asp
/// - https://en.wikipedia.org/wiki/Day_count_convention
use crate::fixed_income::{DayCount, DayCountConvention};
use chrono::{Datelike, NaiveDate};

Expand All @@ -8,17 +16,15 @@ impl DayCountConvention for DayCount {
match self {
DayCount::Act365F => days / 365.0,
DayCount::Act360 => days / 360.0,
DayCount::Act365 => days / 365.0,
DayCount::Act365 => {
let is_leap = chrono::NaiveDate::from_ymd_opt(start.year(), 2, 29).is_some();
let year_days = if is_leap { 366.0 } else { 365.0 };
days / year_days
}
DayCount::Thirty360US => days / 360.0,
DayCount::Thirty360E => days / 360.0,
DayCount::ActActISDA => {
// More complex calculation for actual/actual ISDA
self.act_act_isda_year_fraction(start, end)
}
DayCount::ActActICMA => {
// ICMA method - requires coupon frequency
days / 365.0 // TODO: Simplified
}
DayCount::ActActISDA => self.act_act_isda_year_fraction(start, end),
DayCount::ActActICMA => self.act_act_icma_year_fraction(start, end),
}
}

Expand Down Expand Up @@ -84,4 +90,9 @@ impl DayCount {

days / year_days
}

fn act_act_icma_year_fraction(&self, start: NaiveDate, end: NaiveDate) -> f64 {
// TODO: Implement proper ACT/ACT ICMA calculation based on coupon periods
0.0
}
}
12 changes: 6 additions & 6 deletions src/fixed_income/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@
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/365 - actual days, 365 days per year (no leap year adjustment)
Act365,
/// Actual/360 - actual days, 360 days per year
Act360,
/// 30/360 US (Bond Basis) - 30 days per month, 360 days per year
Thirty360US,
/// 30/360 European - European version of 30/360
Thirty360E,
/// Actual/365 - actual days, 365 days per year (no leap year adjustment)
Act365,
/// Actual/Actual ISDA - actual days, actual year length
ActActISDA,
/// Actual/Actual ICMA - used for bonds
ActActICMA,
}
Expand Down
Loading
Loading