diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..6e20af4 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index bce4f67..3259623 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/crates/qlab-instrument/src/bond.rs b/crates/qlab-instrument/src/bond.rs index 4a21143..5dab57b 100644 --- a/crates/qlab-instrument/src/bond.rs +++ b/crates/qlab-instrument/src/bond.rs @@ -1,4 +1,5 @@ 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; @@ -6,7 +7,6 @@ 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 { due_date: Date, @@ -65,8 +65,7 @@ impl + AddAssign> Bond { face_value: V, ) -> Option { 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(); @@ -78,11 +77,9 @@ impl + AddAssign> Bond { 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)? @@ -90,8 +87,7 @@ impl + AddAssign> Bond { 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; @@ -99,8 +95,8 @@ impl + AddAssign> Bond { 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)? @@ -108,8 +104,8 @@ impl + AddAssign> Bond { 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; diff --git a/crates/qlab-termstructure/src/yield_curve/linear_interpolation.rs b/crates/qlab-termstructure/src/yield_curve/linear_interpolation.rs index d2d4f67..809520b 100644 --- a/crates/qlab-termstructure/src/yield_curve/linear_interpolation.rs +++ b/crates/qlab-termstructure/src/yield_curve/linear_interpolation.rs @@ -43,10 +43,7 @@ impl LinearInterpolation { } 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::, _>>()?; let mut points = Vec::with_capacity(maturities.len()); for (maturity, spot_yield) in maturities.iter().copied().zip(spot_yields) { @@ -64,7 +61,7 @@ impl LinearInterpolation { } impl YieldCurveInner -for LinearInterpolation + for LinearInterpolation { fn yield_curve(&self, t: V) -> Result { let last_point = self.points.last().ok_or(ComputationError::InvalidInput( @@ -83,14 +80,12 @@ for LinearInterpolation 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 YieldCurve -for LinearInterpolation -{ +impl YieldCurve for LinearInterpolation { fn settlement_date(&self) -> Date { self.settlement_date } diff --git a/crates/qlab-time/src/day_count/thirty_360.rs b/crates/qlab-time/src/day_count/thirty_360.rs index 66ad412..1471e7b 100644 --- a/crates/qlab-time/src/day_count/thirty_360.rs +++ b/crates/qlab-time/src/day_count/thirty_360.rs @@ -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 { 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, + ) } } @@ -24,8 +29,8 @@ impl DayCount for Thirty360 { date1: Date, date2: Date, ) -> Result { - 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)) } diff --git a/crates/qlab/src/lib.rs b/crates/qlab/src/lib.rs index e69de29..8b13789 100644 --- a/crates/qlab/src/lib.rs +++ b/crates/qlab/src/lib.rs @@ -0,0 +1 @@ + diff --git a/crates/qlab/tests/calculate_bond.rs b/crates/qlab/tests/calculate_bond.rs index 90d30e8..cd4aa05 100644 --- a/crates/qlab/tests/calculate_bond.rs +++ b/crates/qlab/tests/calculate_bond.rs @@ -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(), @@ -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}"); -} \ No newline at end of file +}