Skip to content

Commit

Permalink
Reformat code for readability and add CI workflow
Browse files Browse the repository at this point in the history
Performed code refactoring across different modules to improve readability and maintainability. Added a continuous integration workflow to automatically run tests and enforce code quality checks. Made small modifications in bond calculations, error handling, and yield curve implementation.
  • Loading branch information
nakashima-hikaru committed Jan 2, 2024
1 parent 75d9e34 commit 8d46253
Show file tree
Hide file tree
Showing 7 changed files with 51 additions and 32 deletions.
18 changes: 18 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: CI

on:
push:
branches: [ main ]

jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt, nextest
- run: cargo fmt --all --check
- run: cargo check --workspace
- run: cargo clippy --all
- run: cargo nextest run
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# qlab
# QLab 🦀

![CI](https://github.com/nakashima-hikaru/qlab/actions/workflows/ci.yaml/badge.svg)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

A Quantitative Finance library implemented in Rust.
Expand Down
22 changes: 9 additions & 13 deletions crates/qlab-instrument/src/bond.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use num_traits::{Float, FromPrimitive};
use qlab_error::ComputationError;
use qlab_termstructure::yield_curve::YieldCurve;
use qlab_time::date::Date;
use qlab_time::day_count::DayCount;
use qlab_time::frequency::Frequency;
use qlab_time::months::Months;
use std::cmp::Ordering;
use std::ops::{AddAssign, MulAssign};
use qlab_error::ComputationError;

struct BondCashFlow<V> {
due_date: Date,
Expand Down Expand Up @@ -65,8 +65,7 @@ impl<V: Float + FromPrimitive + MulAssign<V> + AddAssign<V>> Bond<V> {
face_value: V,
) -> Option<Self> {
let months_in_regular_coupon_period = Months::new(12 / coupon_frequency as u32);
let regular_coupon_payment =
coupon_rate * face_value / V::from_u8(coupon_frequency as u8)?;
let regular_coupon_payment = coupon_rate * face_value / V::from_u8(coupon_frequency as u8)?;

let mut regular_due_date = first_coupon_date;
let mut bond_cash_flows = Vec::new();
Expand All @@ -78,38 +77,35 @@ impl<V: Float + FromPrimitive + MulAssign<V> + AddAssign<V>> Bond<V> {
payment_date,
payment_amount: regular_coupon_payment,
});
regular_due_date = regular_due_date
.add_months(months_in_regular_coupon_period)?;
regular_due_date = regular_due_date.add_months(months_in_regular_coupon_period)?;
}
let first_prior = first_coupon_date
.sub_months(months_in_regular_coupon_period)?;
let first_prior = first_coupon_date.sub_months(months_in_regular_coupon_period)?;
match first_prior.cmp(&issue_date) {
Ordering::Less => {
let coupon_fraction = V::from_i32(first_coupon_date - issue_date)?
/ V::from_i32(first_coupon_date - first_prior)?;
bond_cash_flows[0].payment_amount *= coupon_fraction;
}
Ordering::Greater => {
let second_prior = first_prior
.sub_months(months_in_regular_coupon_period)?;
let second_prior = first_prior.sub_months(months_in_regular_coupon_period)?;
let coupon_fraction = V::from_i32(first_prior - issue_date)?
/ V::from_i32(first_prior - second_prior)?;
bond_cash_flows[0].payment_amount += coupon_fraction * regular_coupon_payment;
}
Ordering::Equal => {}
}
let mut final_coupon = regular_coupon_payment;
let maturity_regular_date = penultimate_coupon_date
.add_months(months_in_regular_coupon_period)?;
let maturity_regular_date =
penultimate_coupon_date.add_months(months_in_regular_coupon_period)?;
match maturity_date.cmp(&maturity_regular_date) {
Ordering::Less => {
let coupon_fraction = V::from_i32(maturity_date - penultimate_coupon_date)?
/ V::from_i32(maturity_regular_date - penultimate_coupon_date)?;
final_coupon *= coupon_fraction;
}
Ordering::Greater => {
let next_regular_date = maturity_regular_date
.add_months(months_in_regular_coupon_period)?;
let next_regular_date =
maturity_regular_date.add_months(months_in_regular_coupon_period)?;
let extra_coupon_fraction = V::from_i32(maturity_date - maturity_regular_date)?
/ V::from_i32(next_regular_date - maturity_regular_date)?;
final_coupon += extra_coupon_fraction * regular_coupon_payment;
Expand Down
15 changes: 5 additions & 10 deletions crates/qlab-termstructure/src/yield_curve/linear_interpolation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,7 @@ impl<V: Float + FromPrimitive, D: DayCount> LinearInterpolation<V, D> {
}
let maturities: Vec<_> = maturities
.iter()
.map(|maturity| {
day_count
.calculate_day_count_fraction(settlement_date, *maturity)
})
.map(|maturity| day_count.calculate_day_count_fraction(settlement_date, *maturity))
.collect::<Result<Vec<V>, _>>()?;
let mut points = Vec::with_capacity(maturities.len());
for (maturity, spot_yield) in maturities.iter().copied().zip(spot_yields) {
Expand All @@ -64,7 +61,7 @@ impl<V: Float + FromPrimitive, D: DayCount> LinearInterpolation<V, D> {
}

impl<V: Float + FromPrimitive + Debug, D: DayCount> YieldCurveInner<V>
for LinearInterpolation<V, D>
for LinearInterpolation<V, D>
{
fn yield_curve(&self, t: V) -> Result<V, ComputationError> {
let last_point = self.points.last().ok_or(ComputationError::InvalidInput(
Expand All @@ -83,14 +80,12 @@ for LinearInterpolation<V, D>

Ok(self.points[idx - 1].spot_yield
+ (self.points[idx].spot_yield - self.points[idx - 1].spot_yield)
/ (self.points[idx].maturity - self.points[idx - 1].maturity)
* (t - self.points[idx - 1].maturity))
/ (self.points[idx].maturity - self.points[idx - 1].maturity)
* (t - self.points[idx - 1].maturity))
}
}

impl<V: Float + FromPrimitive + Debug, D: DayCount> YieldCurve<V, D>
for LinearInterpolation<V, D>
{
impl<V: Float + FromPrimitive + Debug, D: DayCount> YieldCurve<V, D> for LinearInterpolation<V, D> {
fn settlement_date(&self) -> Date {
self.settlement_date
}
Expand Down
15 changes: 10 additions & 5 deletions crates/qlab-time/src/day_count/thirty_360.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@ use qlab_error::ComputationError;
pub struct Thirty360 {}

impl Thirty360 {
#[allow(clippy::cast_sign_loss)] // validation deals with it
#[allow(clippy::cast_sign_loss)] // validation deals with it
fn date_diff(date1: Date, date2: Date) -> Result<u32, ComputationError> {
if date1 > date2 {
return Err(ComputationError::InvalidInput(format!("date1: {date1:?} must precede date2: {date2:?}")));
return Err(ComputationError::InvalidInput(format!(
"date1: {date1:?} must precede date2: {date2:?}"
)));
}
let d1 = date1.day().min(30);
let d2 = date2.day().min(30);
Ok(360 * (date2.year() - date1.year()) as u32 + 30 * (date2.month() - date1.month()) + d2 - d1)
Ok(
360 * (date2.year() - date1.year()) as u32 + 30 * (date2.month() - date1.month()) + d2
- d1,
)
}
}

Expand All @@ -24,8 +29,8 @@ impl DayCount for Thirty360 {
date1: Date,
date2: Date,
) -> Result<V, ComputationError> {
let date_diff = V::from_u32(Self::date_diff(date1, date2)?)
.ok_or(ComputationError::CastNumberError)?;
let date_diff =
V::from_u32(Self::date_diff(date1, date2)?).ok_or(ComputationError::CastNumberError)?;
let denomination = V::from(360.0).ok_or(ComputationError::CastNumberError)?;
Ok(date_diff.div(denomination))
}
Expand Down
1 change: 1 addition & 0 deletions crates/qlab/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

9 changes: 6 additions & 3 deletions crates/qlab/tests/calculate_bond.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ fn main() {
coupon_frequency,
coupon_rate,
face_value,
).unwrap();
)
.unwrap();
let spot_settle_date = Date::from_ymd_opt(2023, 10, 10).unwrap();
let maturities = [
Date::from_ymd_opt(2023, 10, 11).unwrap(),
Expand All @@ -45,7 +46,9 @@ fn main() {
let day_count = Act365::default();
let yield_curve =
LinearInterpolation::new(spot_settle_date, &maturities, spot_yields, day_count).unwrap();
let val = bond_20_yr.discounted_value(spot_settle_date, &yield_curve).unwrap();
let val = bond_20_yr
.discounted_value(spot_settle_date, &yield_curve)
.unwrap();
println!("{}", bond_20_yr.bond_id());
println!("{val}");
}
}

0 comments on commit 8d46253

Please sign in to comment.