use super::{CallableFixedRateBond, CallableZeroCouponBond};
use crate::errors::QlResult;
use crate::interestrate::Compounding;
use crate::pricingengines::bond::TreeCallableFixedRateBondEngine;
use crate::require;
use crate::time::daycounter::DayCounter;
use crate::time::frequency::Frequency;
use crate::types::{Real, Spread};
fn sensitivity(
spread: Spread,
bump: Real,
convexity: bool,
mut price: impl FnMut(Spread) -> QlResult<Real>,
) -> QlResult<Real> {
require!(spread.is_finite(), "OAS must be finite");
require!(
bump.is_finite() && bump > 0.0,
"bump must be finite and positive"
);
let upper = spread + bump;
let lower = spread - bump;
require!(
upper.is_finite() && lower.is_finite() && upper > spread && lower < spread,
"OAS bump must be finite and representable"
);
let center = price(spread)?;
let up = price(upper)?;
let down = price(lower)?;
require!(
[center, up, down].iter().all(|value| value.is_finite()),
"nonfinite sensitivity price"
);
if center == 0.0 {
return Ok(0.0);
}
let (numerator, denominator) = if convexity {
(up + down - 2.0 * center, bump * bump * center)
} else {
(down - up, 2.0 * center * bump)
};
require!(
numerator.is_finite() && denominator.is_finite() && denominator != 0.0,
"sensitivity arithmetic overflow or underflow"
);
let result = numerator / denominator;
require!(result.is_finite(), "nonfinite sensitivity result");
Ok(result)
}
impl CallableFixedRateBond {
#[allow(clippy::too_many_arguments)]
pub fn effective_duration(
&self,
spread: Spread,
engine: &TreeCallableFixedRateBondEngine,
day_counter: DayCounter,
compounding: Compounding,
frequency: Frequency,
bump: Option<Real>,
) -> QlResult<Real> {
sensitivity(spread, bump.unwrap_or(2e-4), false, |value| {
self.clean_price_oas(
value,
engine,
day_counter.clone(),
compounding,
frequency,
None,
)
})
}
#[allow(clippy::too_many_arguments)]
pub fn effective_convexity(
&self,
spread: Spread,
engine: &TreeCallableFixedRateBondEngine,
day_counter: DayCounter,
compounding: Compounding,
frequency: Frequency,
bump: Option<Real>,
) -> QlResult<Real> {
sensitivity(spread, bump.unwrap_or(2e-4), true, |value| {
self.clean_price_oas(
value,
engine,
day_counter.clone(),
compounding,
frequency,
None,
)
})
}
}
impl CallableZeroCouponBond {
#[allow(clippy::too_many_arguments)]
pub fn effective_duration(
&self,
spread: Spread,
engine: &TreeCallableFixedRateBondEngine,
day_counter: DayCounter,
compounding: Compounding,
frequency: Frequency,
bump: Option<Real>,
) -> QlResult<Real> {
sensitivity(spread, bump.unwrap_or(2e-4), false, |value| {
self.clean_price_oas(
value,
engine,
day_counter.clone(),
compounding,
frequency,
None,
)
})
}
#[allow(clippy::too_many_arguments)]
pub fn effective_convexity(
&self,
spread: Spread,
engine: &TreeCallableFixedRateBondEngine,
day_counter: DayCounter,
compounding: Compounding,
frequency: Frequency,
bump: Option<Real>,
) -> QlResult<Real> {
sensitivity(spread, bump.unwrap_or(2e-4), true, |value| {
self.clean_price_oas(
value,
engine,
day_counter.clone(),
compounding,
frequency,
None,
)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_center_still_evaluates_both_bumps() {
let mut probes = Vec::new();
assert_eq!(
sensitivity(0.0, 0.01, false, |s| {
probes.push(s);
Ok(s)
})
.unwrap(),
0.0
);
assert_eq!(probes, vec![0.0, 0.01, -0.01]);
assert!(
sensitivity(0.0, 0.01, true, |s| {
require!(s.is_finite() && s <= 0.0, "failed upper price");
Ok(0.0)
})
.is_err()
);
}
#[test]
fn rejects_nonfinite_prices_and_arithmetic() {
for convexity in [false, true] {
for price in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert!(sensitivity(0.0, 0.01, convexity, |_| Ok(price)).is_err());
}
assert!(
sensitivity(0.0, f64::MIN_POSITIVE, convexity, |_| Ok(f64::MIN_POSITIVE)).is_err()
);
}
assert!(sensitivity(0.0, 0.01, true, |_| Ok(f64::MAX)).is_err());
assert!(
sensitivity(0.0, 0.01, false, |s| Ok(if s == 0.0 {
f64::MIN_POSITIVE
} else {
s.signum()
}))
.is_err()
);
}
}