use chrono::NaiveDate;
use super::handle::Handle;
use super::quote::Quote;
use crate::calendar::DayCountConvention;
use crate::calendar::Frequency;
use crate::curves::DiscountCurve;
use crate::curves::Instrument;
use crate::curves::InterpolationMethod;
use crate::curves::bootstrap;
use crate::traits::FloatExt;
pub trait RateHelper<T: FloatExt>: Send + Sync {
fn maturity(&self, valuation_date: NaiveDate) -> T;
fn to_instrument(&self, valuation_date: NaiveDate) -> Option<Instrument<T>>;
}
fn read_quote<T: FloatExt>(handle: &Handle<dyn Quote<T>>) -> Option<T> {
handle.current().and_then(|q| {
let v = q.value();
if v.is_finite() { Some(v) } else { None }
})
}
#[derive(Debug, Clone)]
pub struct DepositRateHelper<T: FloatExt> {
pub rate_quote: Handle<dyn Quote<T>>,
pub start_date: NaiveDate,
pub maturity_date: NaiveDate,
pub day_count: DayCountConvention,
}
impl<T: FloatExt> DepositRateHelper<T> {
pub fn new(
rate_quote: Handle<dyn Quote<T>>,
start_date: NaiveDate,
maturity_date: NaiveDate,
day_count: DayCountConvention,
) -> Self {
Self {
rate_quote,
start_date,
maturity_date,
day_count,
}
}
}
impl<T: FloatExt> RateHelper<T> for DepositRateHelper<T> {
fn maturity(&self, valuation_date: NaiveDate) -> T {
self
.day_count
.year_fraction(valuation_date, self.maturity_date)
}
fn to_instrument(&self, valuation_date: NaiveDate) -> Option<Instrument<T>> {
let rate = read_quote(&self.rate_quote)?;
let maturity = self
.day_count
.year_fraction(valuation_date, self.maturity_date);
Some(Instrument::Deposit { maturity, rate })
}
}
#[derive(Debug, Clone)]
pub struct FraRateHelper<T: FloatExt> {
pub rate_quote: Handle<dyn Quote<T>>,
pub start_date: NaiveDate,
pub end_date: NaiveDate,
pub day_count: DayCountConvention,
}
impl<T: FloatExt> FraRateHelper<T> {
pub fn new(
rate_quote: Handle<dyn Quote<T>>,
start_date: NaiveDate,
end_date: NaiveDate,
day_count: DayCountConvention,
) -> Self {
Self {
rate_quote,
start_date,
end_date,
day_count,
}
}
}
impl<T: FloatExt> RateHelper<T> for FraRateHelper<T> {
fn maturity(&self, valuation_date: NaiveDate) -> T {
self.day_count.year_fraction(valuation_date, self.end_date)
}
fn to_instrument(&self, valuation_date: NaiveDate) -> Option<Instrument<T>> {
let rate = read_quote(&self.rate_quote)?;
let start = self
.day_count
.year_fraction(valuation_date, self.start_date);
let end = self.day_count.year_fraction(valuation_date, self.end_date);
Some(Instrument::Fra { start, end, rate })
}
}
#[derive(Debug, Clone)]
pub struct SwapRateHelper<T: FloatExt> {
pub rate_quote: Handle<dyn Quote<T>>,
pub settlement_date: NaiveDate,
pub maturity_date: NaiveDate,
pub fixed_frequency: Frequency,
pub day_count: DayCountConvention,
}
impl<T: FloatExt> SwapRateHelper<T> {
pub fn new(
rate_quote: Handle<dyn Quote<T>>,
settlement_date: NaiveDate,
maturity_date: NaiveDate,
fixed_frequency: Frequency,
day_count: DayCountConvention,
) -> Self {
Self {
rate_quote,
settlement_date,
maturity_date,
fixed_frequency,
day_count,
}
}
}
impl<T: FloatExt> RateHelper<T> for SwapRateHelper<T> {
fn maturity(&self, valuation_date: NaiveDate) -> T {
self
.day_count
.year_fraction(valuation_date, self.maturity_date)
}
fn to_instrument(&self, valuation_date: NaiveDate) -> Option<Instrument<T>> {
let rate = read_quote(&self.rate_quote)?;
let maturity = self
.day_count
.year_fraction(valuation_date, self.maturity_date);
Some(Instrument::Swap {
maturity,
rate,
frequency: self.fixed_frequency.periods_per_year(),
})
}
}
#[derive(Debug, Clone)]
pub struct FuturesRateHelper<T: FloatExt> {
pub price_quote: Handle<dyn Quote<T>>,
pub start_date: NaiveDate,
pub end_date: NaiveDate,
pub day_count: DayCountConvention,
pub sigma: T,
}
impl<T: FloatExt> FuturesRateHelper<T> {
pub fn new(
price_quote: Handle<dyn Quote<T>>,
start_date: NaiveDate,
end_date: NaiveDate,
day_count: DayCountConvention,
sigma: T,
) -> Self {
Self {
price_quote,
start_date,
end_date,
day_count,
sigma,
}
}
}
impl<T: FloatExt> RateHelper<T> for FuturesRateHelper<T> {
fn maturity(&self, valuation_date: NaiveDate) -> T {
self.day_count.year_fraction(valuation_date, self.end_date)
}
fn to_instrument(&self, valuation_date: NaiveDate) -> Option<Instrument<T>> {
let price = read_quote(&self.price_quote)?;
let start = self
.day_count
.year_fraction(valuation_date, self.start_date);
let end = self.day_count.year_fraction(valuation_date, self.end_date);
Some(Instrument::Future {
start,
end,
price,
sigma: self.sigma,
})
}
}
pub fn build_curve<T: FloatExt>(
helpers: &[&dyn RateHelper<T>],
valuation_date: NaiveDate,
method: InterpolationMethod,
) -> DiscountCurve<T> {
let instruments: Vec<Instrument<T>> = helpers
.iter()
.filter_map(|h| h.to_instrument(valuation_date))
.collect();
bootstrap(&instruments, method)
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::market::quote::SimpleQuote;
fn months_later(base: NaiveDate, months: i32) -> NaiveDate {
let total = base.year() * 12 + (base.month0() as i32) + months;
let year = total.div_euclid(12);
let month = (total.rem_euclid(12) + 1) as u32;
NaiveDate::from_ymd_opt(year, month, 1).unwrap()
}
use chrono::Datelike;
#[test]
fn build_curve_matches_direct_bootstrap() {
let val_date = NaiveDate::from_ymd_opt(2025, 1, 1).unwrap();
let d3m = months_later(val_date, 3);
let d6m = months_later(val_date, 6);
let d2y = months_later(val_date, 24);
let q_dep = Arc::new(SimpleQuote::<f64>::new(0.04));
let q_fra = Arc::new(SimpleQuote::<f64>::new(0.042));
let q_swap = Arc::new(SimpleQuote::<f64>::new(0.045));
let dep_handle: Handle<dyn Quote<f64>> = Handle::new(Arc::clone(&q_dep) as Arc<dyn Quote<f64>>);
let fra_handle: Handle<dyn Quote<f64>> = Handle::new(Arc::clone(&q_fra) as Arc<dyn Quote<f64>>);
let swap_handle: Handle<dyn Quote<f64>> =
Handle::new(Arc::clone(&q_swap) as Arc<dyn Quote<f64>>);
let dep = DepositRateHelper::new(dep_handle, val_date, d3m, DayCountConvention::Actual360);
let fra = FraRateHelper::new(fra_handle, d3m, d6m, DayCountConvention::Actual360);
let swap = SwapRateHelper::new(
swap_handle,
val_date,
d2y,
Frequency::SemiAnnual,
DayCountConvention::Actual365Fixed,
);
let helpers: Vec<&dyn RateHelper<f64>> = vec![&dep, &fra, &swap];
let curve = build_curve(&helpers, val_date, InterpolationMethod::LinearOnZeroRates);
assert!(curve.len() >= 4);
let df_short = curve.discount_factor(0.25);
let df_long = curve.discount_factor(2.0);
assert!(df_short > df_long);
assert!(df_long > 0.0 && df_short < 1.0);
}
#[test]
fn helper_reflects_updated_quote() {
let val_date = NaiveDate::from_ymd_opt(2025, 1, 1).unwrap();
let q = Arc::new(SimpleQuote::<f64>::new(0.02));
let handle: Handle<dyn Quote<f64>> = Handle::new(Arc::clone(&q) as Arc<dyn Quote<f64>>);
let helper = DepositRateHelper::new(
handle,
val_date,
NaiveDate::from_ymd_opt(2025, 4, 1).unwrap(),
DayCountConvention::Actual360,
);
match helper.to_instrument(val_date).unwrap() {
Instrument::Deposit { rate, .. } => assert!((rate - 0.02).abs() < 1e-12),
_ => panic!("expected deposit"),
}
q.set_value(0.035);
match helper.to_instrument(val_date).unwrap() {
Instrument::Deposit { rate, .. } => assert!((rate - 0.035).abs() < 1e-12),
_ => panic!("expected deposit"),
}
}
}