#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::finance::{year_fraction, CouponSchedule, Date, DayCountConvention};
use super::{ValuationContextError, YieldCurve};
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum CalibrationInstrument {
ZeroRate { maturity: Date, rate: f64 },
ParSwap {
maturity: Date,
rate: f64,
frequency: u32,
},
}
impl CalibrationInstrument {
pub fn maturity(&self) -> Date {
match self {
Self::ZeroRate { maturity, .. } | Self::ParSwap { maturity, .. } => *maturity,
}
}
}
impl YieldCurve {
pub fn bootstrap(
reference: Date,
instruments: &[CalibrationInstrument],
day_count: DayCountConvention,
) -> Result<Self, ValuationContextError> {
if instruments.is_empty() {
return Err(ValuationContextError::InvalidCurve(
"bootstrapping needs at least one instrument",
));
}
let mut sorted = instruments.to_vec();
sorted.sort_by_key(|instrument| instrument.maturity());
if sorted
.windows(2)
.any(|w| w[0].maturity() == w[1].maturity())
{
return Err(ValuationContextError::InvalidCurve(
"two instruments share a maturity",
));
}
if sorted[0].maturity() <= reference {
return Err(ValuationContextError::InvalidCurve(
"instruments must mature after the reference date",
));
}
let mut nodes: Vec<(f64, f64)> = Vec::with_capacity(sorted.len());
for instrument in &sorted {
let time = year_fraction(reference, instrument.maturity(), day_count);
match instrument {
CalibrationInstrument::ZeroRate { rate, .. } => {
if !rate.is_finite() {
return Err(ValuationContextError::InvalidCurve(
"a quoted zero rate must be finite",
));
}
nodes.push((time, *rate));
}
CalibrationInstrument::ParSwap {
maturity,
rate,
frequency,
} => {
let rate = solve_par_swap_node(
reference, *maturity, *rate, *frequency, day_count, &nodes,
)?;
nodes.push((time, rate));
}
}
}
Self::from_zero_rates(reference, nodes, day_count)
}
pub fn par_swap_rate(
&self,
maturity: Date,
frequency: u32,
) -> Result<f64, ValuationContextError> {
let (annuity, final_discount) = swap_legs(self, maturity, frequency)?;
if annuity <= 0.0 {
return Err(ValuationContextError::InvalidCurve(
"swap annuity is not positive",
));
}
Ok((1.0 - final_discount) / annuity)
}
}
fn swap_legs(
curve: &YieldCurve,
maturity: Date,
frequency: u32,
) -> Result<(f64, f64), ValuationContextError> {
let schedule = CouponSchedule::covering(curve.reference_date(), maturity, frequency)
.map_err(ValuationContextError::Instrument)?;
let mut annuity = 0.0;
for index in 0..schedule.period_count() {
let (start, end) = schedule
.period(index)
.expect("index below period_count is valid");
let accrual = year_fraction(start, end, curve.day_count());
annuity += accrual * curve.discount_factor(end)?;
}
Ok((annuity, curve.discount_factor(maturity)?))
}
fn solve_par_swap_node(
reference: Date,
maturity: Date,
quoted_rate: f64,
frequency: u32,
day_count: DayCountConvention,
fixed_nodes: &[(f64, f64)],
) -> Result<f64, ValuationContextError> {
if !quoted_rate.is_finite() {
return Err(ValuationContextError::InvalidCurve(
"a quoted swap rate must be finite",
));
}
let time = year_fraction(reference, maturity, day_count);
let mismatch = |candidate: f64| -> Result<f64, ValuationContextError> {
let mut nodes = fixed_nodes.to_vec();
nodes.push((time, candidate));
let curve = YieldCurve::from_zero_rates(reference, nodes, day_count)?;
let (annuity, final_discount) = swap_legs(&curve, maturity, frequency)?;
Ok(quoted_rate * annuity - (1.0 - final_discount))
};
let (mut low, mut high) = (-0.5, 1.0);
let (low_value, high_value) = (mismatch(low)?, mismatch(high)?);
if low_value.signum() == high_value.signum() {
return Err(ValuationContextError::InvalidCurve(
"no zero rate between -50% and +100% prices this swap at par",
));
}
for _ in 0..100 {
let middle = 0.5 * (low + high);
let value = mismatch(middle)?;
if value == 0.0 {
return Ok(middle);
}
if value.signum() == low_value.signum() {
low = middle;
} else {
high = middle;
}
}
Ok(0.5 * (low + high))
}