use crate::errors::QlResult;
use crate::math::interpolations::Interpolation;
use crate::math::interpolations::linear::LinearInterpolation;
use crate::require;
use crate::termstructures::volatility::VolatilityType;
use crate::termstructures::volatility::smilesection::{SmileSection, SmileSectionBase};
use crate::time::daycounter::DayCounter;
use crate::types::{Rate, Real, Time, Volatility};
pub struct InterpolatedSmileSection {
base: SmileSectionBase,
interpolation: LinearInterpolation,
atm_level: Rate,
}
impl InterpolatedSmileSection {
pub fn with_exercise_time(
exercise_time: Time,
strikes: Vec<Rate>,
std_devs: Vec<Real>,
atm_level: Rate,
day_counter: DayCounter,
volatility_type: VolatilityType,
shift: Rate,
) -> QlResult<InterpolatedSmileSection> {
require!(!strikes.is_empty(), "strikes must not be empty");
require!(
strikes.len() == std_devs.len(),
"strikes and std_devs must have equal length ({} vs {})",
strikes.len(),
std_devs.len()
);
require!(
strikes.windows(2).all(|w| w[0] <= w[1]),
"strikes must be sorted in ascending order"
);
let base = SmileSectionBase::with_exercise_time(
exercise_time,
day_counter,
volatility_type,
shift,
)?;
let sqrt_t = exercise_time.sqrt();
let vols: Vec<Volatility> = std_devs.iter().map(|&sd| sd / sqrt_t).collect();
let interpolation = LinearInterpolation::new(strikes, vols)?.with_extrapolation(true);
Ok(InterpolatedSmileSection {
base,
interpolation,
atm_level,
})
}
}
impl SmileSection for InterpolatedSmileSection {
fn base(&self) -> &SmileSectionBase {
&self.base
}
fn volatility_impl(&self, strike: Rate) -> QlResult<Volatility> {
Ok(self.interpolation.value(strike)?.max(0.0))
}
fn min_strike(&self) -> Rate {
self.interpolation.x_min()
}
fn max_strike(&self) -> Rate {
self.interpolation.x_max()
}
fn atm_level(&self) -> Option<Rate> {
Some(self.atm_level)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::option::OptionType;
use crate::pricingengines::blackformula::black_formula;
use crate::time::daycounters::actual365fixed::Actual365Fixed;
const TIME: Time = 4.0;
const ATM: Rate = 0.04;
fn strikes() -> Vec<Rate> {
vec![0.02, 0.03, 0.04, 0.05, 0.06]
}
fn std_devs() -> Vec<Real> {
vec![0.40, 0.36, 0.30, 0.32, 0.34]
}
fn section() -> InterpolatedSmileSection {
InterpolatedSmileSection::with_exercise_time(
TIME,
strikes(),
std_devs(),
ATM,
Actual365Fixed::new(),
VolatilityType::ShiftedLognormal,
0.0,
)
.unwrap()
}
#[test]
fn volatility_recovers_divided_std_dev_at_nodes() {
let section = section();
for (&k, &sd) in strikes().iter().zip(std_devs().iter()) {
let expected = sd / TIME.sqrt();
assert!(
(section.volatility(k).unwrap() - expected).abs() < 1e-15,
"strike={k}"
);
}
}
#[test]
fn variance_round_trips_to_std_dev_squared() {
let section = section();
for (&k, &sd) in strikes().iter().zip(std_devs().iter()) {
assert!(
(section.variance(k).unwrap() - sd * sd).abs() < 1e-15,
"strike={k}"
);
}
}
#[test]
fn volatility_between_nodes_is_the_linear_blend() {
let section = section();
let mid = 0.035;
let lo = std_devs()[1] / TIME.sqrt();
let hi = std_devs()[2] / TIME.sqrt();
let expected = 0.5 * (lo + hi);
assert!((section.volatility(mid).unwrap() - expected).abs() < 1e-15);
}
#[test]
fn extrapolation_extends_the_end_segments() {
let section = section();
let below = section.volatility(0.01).unwrap();
let above = section.volatility(0.07).unwrap();
assert!(below.is_finite());
assert!(above.is_finite());
let s0 = std_devs()[0] / TIME.sqrt();
let s1 = std_devs()[1] / TIME.sqrt();
let slope = (s1 - s0) / (strikes()[1] - strikes()[0]);
let expected_below = s0 + (0.01 - strikes()[0]) * slope;
assert!((below - expected_below).abs() < 1e-15);
}
#[test]
fn far_extrapolation_is_floored_at_zero() {
let section = InterpolatedSmileSection::with_exercise_time(
TIME,
vec![0.02, 0.03, 0.04],
vec![0.60, 0.40, 0.20],
ATM,
Actual365Fixed::new(),
VolatilityType::ShiftedLognormal,
0.0,
)
.unwrap();
assert_eq!(section.volatility(1.0).unwrap(), 0.0);
}
#[test]
fn strike_bounds_are_the_grid_ends() {
let section = section();
assert_eq!(section.min_strike(), 0.02);
assert_eq!(section.max_strike(), 0.06);
}
#[test]
fn atm_level_round_trips() {
assert_eq!(section().atm_level(), Some(ATM));
}
#[test]
fn option_price_prices_through_the_smile_with_shift() {
let shift = 0.01;
let section = InterpolatedSmileSection::with_exercise_time(
TIME,
strikes(),
std_devs(),
ATM,
Actual365Fixed::new(),
VolatilityType::ShiftedLognormal,
shift,
)
.unwrap();
let strike = 0.05;
let std_dev = section.variance(strike).unwrap().sqrt();
let expected = black_formula(OptionType::Call, strike, ATM, std_dev, 1.0, shift).unwrap();
let got = section.option_price(strike, OptionType::Call, 1.0).unwrap();
assert!((got - expected).abs() < 1e-15);
}
#[test]
fn unsorted_strikes_are_rejected() {
assert!(
InterpolatedSmileSection::with_exercise_time(
TIME,
vec![0.04, 0.03, 0.05],
vec![0.30, 0.36, 0.32],
ATM,
Actual365Fixed::new(),
VolatilityType::ShiftedLognormal,
0.0,
)
.is_err()
);
}
#[test]
fn length_mismatch_is_rejected() {
assert!(
InterpolatedSmileSection::with_exercise_time(
TIME,
vec![0.02, 0.03, 0.04],
vec![0.30, 0.36],
ATM,
Actual365Fixed::new(),
VolatilityType::ShiftedLognormal,
0.0,
)
.is_err()
);
}
#[test]
fn empty_strikes_are_rejected() {
assert!(
InterpolatedSmileSection::with_exercise_time(
TIME,
vec![],
vec![],
ATM,
Actual365Fixed::new(),
VolatilityType::ShiftedLognormal,
0.0,
)
.is_err()
);
}
}