use crate::errors::QlResult;
use crate::math::interpolations::{Interpolation, Interpolator};
use crate::time::date::Date;
use crate::types::{DiscountFactor, Real, Size, Time};
use crate::{fail, require};
const AVG_RATE: Real = 0.05;
const MAX_RATE: Real = 1.0;
pub trait BootstrapTraits {
fn initial_value() -> Real;
fn guess(i: Size, times: &[Time], data: &[Real], valid_data: bool) -> Real;
fn min_value_after(i: Size, times: &[Time], data: &[Real], valid_data: bool) -> Real;
fn max_value_after(i: Size, times: &[Time], data: &[Real], valid_data: bool) -> Real;
fn update_guess(data: &mut [Real], value: Real, i: Size);
fn max_iterations() -> Size;
}
pub struct Discount;
impl BootstrapTraits for Discount {
fn initial_value() -> Real {
1.0
}
fn guess(i: Size, times: &[Time], data: &[Real], valid_data: bool) -> Real {
if valid_data {
return data[i];
}
if i == 1 {
return 1.0 / (1.0 + AVG_RATE * times[1]);
}
let r = -data[i - 1].ln() / times[i - 1];
(-r * times[i]).exp()
}
fn min_value_after(i: Size, times: &[Time], data: &[Real], valid_data: bool) -> Real {
if valid_data {
let min = data.iter().copied().fold(Real::INFINITY, Real::min);
return min / 2.0;
}
let dt = times[i] - times[i - 1];
data[i - 1] * (-MAX_RATE * dt).exp()
}
fn max_value_after(i: Size, times: &[Time], data: &[Real], _valid_data: bool) -> Real {
let dt = times[i] - times[i - 1];
data[i - 1] * (MAX_RATE * dt).exp()
}
fn update_guess(data: &mut [Real], value: Real, i: Size) {
data[i] = value;
}
fn max_iterations() -> Size {
100
}
}
pub struct CurveData<I: Interpolator> {
dates: Vec<Date>,
times: Vec<Time>,
data: Vec<Real>,
interpolation: Option<I::Output>,
max_date: Option<Date>,
valid: bool,
}
impl<I: Interpolator> Default for CurveData<I> {
fn default() -> Self {
CurveData::new()
}
}
impl<I: Interpolator> CurveData<I> {
pub fn new() -> CurveData<I> {
CurveData {
dates: Vec::new(),
times: Vec::new(),
data: Vec::new(),
interpolation: None,
max_date: None,
valid: false,
}
}
pub fn is_valid(&self) -> bool {
self.valid
}
pub fn set_valid(&mut self, valid: bool) {
self.valid = valid;
}
pub fn set_pillars(&mut self, dates: Vec<Date>, times: Vec<Time>) {
self.dates = dates;
self.times = times;
self.interpolation = None;
}
pub fn reset_data(&mut self, initial_value: Real, len: usize) {
self.data = vec![initial_value; len];
self.valid = false;
}
pub fn dates(&self) -> &[Date] {
&self.dates
}
pub fn times(&self) -> &[Time] {
&self.times
}
pub fn data(&self) -> &[Real] {
&self.data
}
pub fn data_mut(&mut self) -> &mut [Real] {
&mut self.data
}
pub fn max_date(&self) -> Option<Date> {
self.max_date
}
pub fn set_max_date(&mut self, date: Date) {
self.max_date = Some(date);
}
pub fn is_initialized(&self) -> bool {
!self.times.is_empty()
}
pub fn nodes(&self) -> Vec<(Date, Real)> {
self.dates
.iter()
.copied()
.zip(self.data.iter().copied())
.collect()
}
pub fn rebuild(&mut self, interpolator: &I, upto: usize) -> QlResult<()> {
self.interpolation =
Some(interpolator.interpolate(&self.times[..=upto], &self.data[..=upto])?);
Ok(())
}
pub fn discount(&self, t: Time) -> QlResult<DiscountFactor> {
let Some(interpolation) = self.interpolation.as_ref() else {
fail!("curve not bootstrapped: no interpolation available");
};
let t_max = interpolation.x_max();
if t <= t_max {
return interpolation.value(t);
}
let d_max = interpolation.value(t_max)?;
let inst_fwd_max = -interpolation.derivative(t_max)? / d_max;
Ok(d_max * (-inst_fwd_max * (t - t_max)).exp())
}
pub fn require_initialized(&self) -> QlResult<()> {
require!(self.is_initialized(), "curve not bootstrapped");
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::interpolations::loglinear::LogLinear;
#[test]
fn discount_initial_value_is_one() {
assert_eq!(Discount::initial_value(), 1.0);
}
#[test]
fn first_pillar_guess_uses_the_average_rate() {
let times = [0.0, 0.5];
let data = [1.0, 1.0];
let guess = Discount::guess(1, ×, &data, false);
assert!((guess - 1.0 / (1.0 + AVG_RATE * 0.5)).abs() < 1e-15);
}
#[test]
fn later_pillar_guess_extrapolates_the_previous_forward_flat() {
let times = [0.0, 0.5, 1.0];
let data = [1.0, 0.98, 1.0];
let r = -0.98_f64.ln() / 0.5;
let guess = Discount::guess(2, ×, &data, false);
assert!((guess - (-r * 1.0).exp()).abs() < 1e-15);
}
#[test]
fn valid_data_guess_reuses_the_stored_node() {
let times = [0.0, 0.5, 1.0];
let data = [1.0, 0.98, 0.95];
assert_eq!(Discount::guess(2, ×, &data, true), 0.95);
}
#[test]
fn bracket_bounds_a_max_rate_forward_around_the_previous_node() {
let times = [0.0, 0.5, 1.0];
let data = [1.0, 0.98, 1.0];
let dt = 0.5;
let min = Discount::min_value_after(2, ×, &data, false);
let max = Discount::max_value_after(2, ×, &data, false);
assert!((min - 0.98 * (-MAX_RATE * dt).exp()).abs() < 1e-15);
assert!((max - 0.98 * (MAX_RATE * dt).exp()).abs() < 1e-15);
assert!(min < max);
}
#[test]
fn valid_data_min_halves_the_smallest_node() {
let times = [0.0, 0.5, 1.0];
let data = [1.0, 0.98, 0.90];
let min = Discount::min_value_after(2, ×, &data, true);
assert!((min - 0.90 / 2.0).abs() < 1e-15);
}
#[test]
fn update_guess_writes_the_node() {
let mut data = [1.0, 0.98, 1.0];
Discount::update_guess(&mut data, 0.95, 2);
assert_eq!(data[2], 0.95);
}
#[test]
fn curve_data_discount_interpolates_in_range_and_extends_flat_beyond() {
let mut cd = CurveData::<LogLinear>::new();
cd.set_pillars(
vec![Date::null(), Date::null(), Date::null()],
vec![0.0, 1.0, 2.0],
);
cd.reset_data(1.0, 3);
cd.data_mut()[1] = 0.95;
cd.data_mut()[2] = 0.88;
cd.rebuild(&LogLinear, 2).unwrap();
let mid = cd.discount(1.5).unwrap();
assert!((mid - (0.95_f64 * 0.88).sqrt()).abs() < 1e-12);
let last_forward = (0.95_f64 / 0.88).ln();
let beyond = cd.discount(3.0).unwrap();
assert!((beyond - 0.88 * (-last_forward * 1.0).exp()).abs() < 1e-12);
}
#[test]
fn curve_data_discount_before_bootstrap_is_an_error() {
let cd = CurveData::<LogLinear>::new();
assert!(cd.discount(1.0).is_err());
assert!(cd.require_initialized().is_err());
}
}