Skip to content

Commit

Permalink
Refactor yield curve construction and handling
Browse files Browse the repository at this point in the history
This commit streamlines yield curve handling by using the qlab-math crate to perform interpolation instead of the previously implemented linear interpolation contained within the qlab-termstructure crate. Consequently, this led to the deletion of `grid_point.rs` and `linear_interpolation.rs`. This refactoring simplifies the termstructure codebase, making it more reusable, and in aligning with best practices, it improves code modularity and maintainability.
  • Loading branch information
nakashima-hikaru committed Jan 3, 2024
1 parent 8e84308 commit 2d1ebfa
Show file tree
Hide file tree
Showing 13 changed files with 229 additions and 146 deletions.
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ keywords = ["finance"]
categories = ["finance"]

[workspace.dependencies]
num-traits = "0.2.17"

qlab-error = { version = "0.1.0", path = "crates/qlab-error", default-features = false }
qlab-time = { version = "0.1.0", path = "crates/qlab-time", default-features = false }
qlab-termstructure = { version = "0.1.0", path = "crates/qlab-termstructure", default-features = false }
qlab-instrument = { version = "0.1.0", path = "crates/qlab-instrument", default-features = false }
qlab-math = { version = "0.1.0", path = "crates/qlab-math", default-features = false }

[workspace.lints.rust]
unsafe_code = "forbid"
Expand Down
3 changes: 2 additions & 1 deletion crates/qlab-instrument/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ readme = "../../README.md"
description = "Market instruments for the qlab"

[dependencies]
num-traits = "0.2.17"
num-traits = { workspace = true }
qlab-time = { workspace = true }
qlab-termstructure = { workspace = true }
qlab-error = { workspace = true }
qlab-math = { workspace = true }

[lints]
workspace = true
5 changes: 3 additions & 2 deletions crates/qlab-instrument/src/bond.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use num_traits::{Float, FromPrimitive};
use qlab_error::QLabResult;
use qlab_math::interpolation::Interpolator;
use qlab_termstructure::yield_curve::YieldCurve;
use qlab_time::date::Date;
use qlab_time::day_count::DayCount;
Expand Down Expand Up @@ -143,10 +144,10 @@ impl<V: Float + FromPrimitive + MulAssign<V> + AddAssign<V>> Bond<V> {
///
/// # Errors
/// Error occurs if a discount factor calculation fails
pub fn discounted_value<D: DayCount>(
pub fn discounted_value<D: DayCount, I: Interpolator<V>>(
&self,
bond_settle_date: Date,
yield_curve: &impl YieldCurve<V, D>,
yield_curve: &YieldCurve<V, D, I>,
) -> QLabResult<V> {
let mut pv = V::zero();
for i in 0..self.bond_cash_flows.len() {
Expand Down
68 changes: 68 additions & 0 deletions crates/qlab-math/src/interpolation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use num_traits::Float;
use qlab_error::ComputeError::InvalidInput;
use qlab_error::QLabResult;

pub mod linear;

#[derive(Copy, Clone)]
pub(crate) struct Point<V: Float> {
x: V,
y: V,
}

mod private {
use crate::interpolation::Point;
use num_traits::Float;

pub(crate) trait InterpolatorInner<V: Float> {
fn set_points(&mut self, points: &[Point<V>]);
}
}

#[allow(private_bounds)]
pub trait Interpolator<V: Float>: private::InterpolatorInner<V> {
/// Fits the given data points to the `QLab` object.
///
/// # Arguments
///
/// * `xs` - An array slice containing the x-values of the data points.
/// * `ys` - An array slice containing the y-values of the data points.
///
/// # Errors
///
/// Returns an `Err` variant if the lengths of `xs` and `ys` do not match.
fn fit(&mut self, xs: &[V], ys: &[V]) -> QLabResult<()> {
if xs.len() != ys.len() {
return Err(InvalidInput(
format!(
"The length `xs`: {} must coincide with that of `ys`: {}",
xs.len(),
ys.len()
)
.into(),
)
.into());
}
let mut points = Vec::with_capacity(xs.len());
for (&x, &y) in xs.iter().zip(ys) {
points.push(Point { x, y });
}
self.set_points(points.as_ref());
Ok(())
}

/// Returns the value of type `V` and wraps it in a `QLabResult`.
///
/// # Arguments
///
/// * `t` - The value of type `V`.
///
/// # Returns
///
/// Returns a `QLabResult` that contains the value `t`.
///
/// # Errors
///
/// An Error returns if interpolation fails.
fn value(&self, t: V) -> QLabResult<V>;
}
65 changes: 65 additions & 0 deletions crates/qlab-math/src/interpolation/linear.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use crate::interpolation::private::InterpolatorInner;
use crate::interpolation::{Interpolator, Point};
use num_traits::Float;
use qlab_error::ComputeError::InvalidInput;
use qlab_error::QLabResult;
use std::fmt::Debug;

#[derive(Default)]
pub struct Linear<V: Float> {
points: Vec<Point<V>>,
}

impl<V: Float> Linear<V> {
#[must_use]
pub fn new() -> Self {
Self { points: Vec::new() }
}
}

impl<V: Float + Debug> InterpolatorInner<V> for Linear<V> {
fn set_points(&mut self, points: &[Point<V>]) {
self.points = points.to_vec();
}
}

impl<V: Float + Debug> Interpolator<V> for Linear<V> {
/// Calculates the value at time `t` using linear interpolation based on a grid of points.
/// If `t` is greater than or equal to the x-coordinate of the last grid point, the y-coordinate of the last grid point will be returned.
///
/// # Arguments
///
/// * `t` - The time value at which to calculate the interpolated value.
///
/// # Returns
///
/// * `QLabResult<V>` - The interpolated value at time `t`.
/// - If `t` is smaller than the x-coordinate of the first grid point, an `InvalidInput` error will be returned.
/// - If there are no grid points available, an `InvalidInput` error will be returned.
///
/// # Errors
///
/// * `InvalidInput` - Represents an error when the input is invalid or out-of-bounds.
fn value(&self, t: V) -> QLabResult<V> {
let last_point = self
.points
.last()
.ok_or(InvalidInput("Grid points doesn't exist".into()))?;

if t >= last_point.x {
return Ok(last_point.y);
}
let idx = self.points.partition_point(|&point| point.x < t);
if idx == 0 {
return Err(InvalidInput(
format!("t: {t:?} is smaller than the `x` value of the first grid point").into(),
)
.into());
}

Ok(self.points[idx - 1].y
+ (self.points[idx].y - self.points[idx - 1].y)
/ (self.points[idx].x - self.points[idx - 1].x)
* (t - self.points[idx - 1].x))
}
}
3 changes: 2 additions & 1 deletion crates/qlab-termstructure/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ readme = "../../README.md"
description = "Term structure objects for the qlab"

[dependencies]
num-traits = "0.2.17"
num-traits = { workspace = true }
qlab-time = { workspace = true }
qlab-error = { workspace = true }
qlab-math = { workspace = true }

[lints]
workspace = true
91 changes: 62 additions & 29 deletions crates/qlab-termstructure/src/yield_curve.rs
Original file line number Diff line number Diff line change
@@ -1,45 +1,75 @@
mod grid_point;
pub mod linear_interpolation;

use num_traits::{Float, FromPrimitive};
use qlab_error::ComputeError::InvalidInput;
use qlab_error::QLabResult;
use qlab_math::interpolation::Interpolator;
use qlab_time::date::Date;
use qlab_time::day_count::DayCount;
use std::marker::PhantomData;

mod private {
use num_traits::{Float, FromPrimitive};
use qlab_error::QLabResult;
use qlab_math::interpolation::Interpolator;

pub trait YieldCurveInner<V: Float + FromPrimitive> {
fn yield_curve(&self, t: V) -> QLabResult<V>;
pub trait YieldCurveInner<V: Float + FromPrimitive, I: Interpolator<V>> {
fn interpolator(&self) -> &I;
fn yield_curve(&self, t: V) -> QLabResult<V> {
self.interpolator().value(t)
}
}
}

/// A trait representing a yield curve with discount factor calculations.
///
/// The trait is generic over the type of Floating point values (`V`) and the day count convention (`D`).
pub trait YieldCurve<V: Float + FromPrimitive, D: DayCount>: private::YieldCurveInner<V> {
/// Calculates the settlement date.
///
/// The settlement date is the date on which a financial transaction is executed and when
/// the transfer of ownership takes place.
pub struct YieldCurve<V: Float + FromPrimitive, D: DayCount, I: Interpolator<V>> {
_phantom: PhantomData<V>,
settlement_date: Date,
day_count: D,
interpolator: I,
}

impl<V: Float + FromPrimitive, D: DayCount, I: Interpolator<V>> YieldCurve<V, D, I> {
/// Creates a new instance of the `QLab` struct.
///
/// # Returns
/// # Arguments
///
/// - The settlement date as a `Date` object.
fn settlement_date(&self) -> Date;
/// Returns a reference to the count fraction of the day.
/// * `settlement_date` - The settlement date of the instrument.
/// * `maturities` - A slice of maturity dates.
/// * `spot_yields` - A vector of spot yields.
/// * `day_count` - The day count convention to use.
/// * `interpolator` - An interpolator for fitting the yields.
///
/// The count fraction of the day is a value that represents the fraction of a day
/// that has passed from the beginning of the day until the current time.
/// This method returns a reference to the count fraction.
/// # Returns
///
/// # Return
/// A `QLabResult` containing the new instance of `QLab`, or an error if the inputs are invalid.
///
/// A reference to the count fraction of the day.
fn day_count_fraction(&self) -> &D;

/// # Errors
/// Returns an `Err` variant if the lengths of `maturities` and `spot_yields` do not match.
pub fn new(
settlement_date: Date,
maturities: &[Date],
spot_yields: &[V],
day_count: D,
mut interpolator: I,
) -> QLabResult<Self> {
if maturities.len() != spot_yields.len() {
return Err(
InvalidInput("maturities and spot_yields are different lengths".into()).into(),
);
}
let maturities: Vec<_> = maturities
.iter()
.map(|maturity| day_count.calculate_day_count_fraction(settlement_date, *maturity))
.collect::<Result<Vec<V>, _>>()?;
interpolator.fit(&maturities, spot_yields)?;
Ok(Self {
_phantom: PhantomData,
settlement_date,
day_count,
interpolator,
})
}
/// Calculates the discount factor between two dates.
///
/// This function calculates the discount factor between two dates, `d1` and `d2`.
Expand All @@ -62,33 +92,36 @@ pub trait YieldCurve<V: Float + FromPrimitive, D: DayCount>: private::YieldCurve
///
/// # Errors
/// An Error returns if invalid inputs are passed
fn discount_factor(&self, d1: Date, d2: Date) -> QLabResult<V> {
pub fn discount_factor(&self, d1: Date, d2: Date) -> QLabResult<V> {
if d2 < d1 {
return Err(
InvalidInput(format!("d1: {d1} must be smaller than d2: {d2}").into()).into(),
);
}
if d1 < self.settlement_date() || d2 < self.settlement_date() {
if d1 < self.settlement_date || d2 < self.settlement_date {
return Err(InvalidInput(
format!(
"Either {d1} or {d2} exceeds settlement date: {:?}",
self.settlement_date()
self.settlement_date
)
.into(),
)
.into());
}
let t2 = self
.day_count_fraction()
.calculate_day_count_fraction(self.settlement_date(), d2)?;
.day_count
.calculate_day_count_fraction(self.settlement_date, d2)?;
let y2 = self.yield_curve(t2)?;
if d1 == self.settlement_date() {
if d1 == self.settlement_date {
return Ok((-t2 * y2).exp());
}
let t1 = self
.day_count_fraction()
.calculate_day_count_fraction(self.settlement_date(), d1)?;
.day_count
.calculate_day_count_fraction(self.settlement_date, d1)?;
let y1 = self.yield_curve(t1)?;
Ok((t1 * y1 - t2 * y2).exp())
}
fn yield_curve(&self, t: V) -> QLabResult<V> {
self.interpolator.value(t)
}
}
7 changes: 0 additions & 7 deletions crates/qlab-termstructure/src/yield_curve/grid_point.rs

This file was deleted.

Loading

0 comments on commit 2d1ebfa

Please sign in to comment.