use crate::{
ad::{dual::DualFwd, scalar::Scalar},
core::{elements::volatilitycubelement::ADVolatilityCubeElement, pillars::Pillars},
indices::marketindex::MarketIndex,
math::interpolation::trilinear::{TrilinearInterpolator, TrilinearPoint},
time::{date::Date, period::Period},
utils::errors::{QSError, Result},
volatility::{
volatilitycube::VolatilityCube,
volatilityindexing::{F64Key, SmileType, VolatilityType},
},
};
use std::collections::BTreeMap;
use crate::math::interpolation::bilinear::BilinearValue;
type CubeMap<T> = BTreeMap<Period, BTreeMap<Period, BTreeMap<F64Key, T>>>;
pub struct InterpolatedVolatilityCube<T: Scalar> {
reference_date: Date,
market_index: MarketIndex,
points: CubeMap<T>,
labels: Option<Vec<String>>,
volatility_type: VolatilityType,
smile_type: SmileType,
}
impl<T: Scalar> InterpolatedVolatilityCube<T> {
#[must_use]
pub const fn new(
reference_date: Date,
market_index: MarketIndex,
points: CubeMap<T>,
volatility_type: VolatilityType,
smile_type: SmileType,
) -> Self {
Self {
reference_date,
market_index,
points,
labels: None,
volatility_type,
smile_type,
}
}
#[must_use]
pub fn with_labels(mut self, labels: &[String]) -> Self {
self.labels = Some(labels.to_owned());
self
}
#[must_use]
pub const fn market_index(&self) -> &MarketIndex {
&self.market_index
}
}
impl<T: BilinearValue> VolatilityCube<T> for InterpolatedVolatilityCube<T> {
fn volatility_from_period(&self, expiry: Period, maturity: Period, key: f64) -> Result<T> {
let points: Vec<TrilinearPoint<T>> = self
.points
.iter()
.flat_map(|(exp, tenor_map)| {
tenor_map.iter().flat_map(move |(tenor, smile)| {
smile.iter().map(move |(axis, value)| TrilinearPoint {
x: exp.period_in_year(),
y: tenor.period_in_year(),
z: axis.value(),
value: *value,
})
})
})
.collect();
TrilinearInterpolator::interpolate(
expiry.period_in_year(),
maturity.period_in_year(),
key,
points,
)
.ok_or_else(|| {
QSError::InterpolationErr(
"Could not trilinearly interpolate volatility for requested expiry/maturity/key"
.into(),
)
})
}
fn volatility_type(&self) -> VolatilityType {
self.volatility_type.clone()
}
fn market_index(&self) -> &MarketIndex {
&self.market_index
}
fn reference_date(&self) -> Date {
self.reference_date
}
fn smile_type(&self) -> SmileType {
self.smile_type
}
}
impl Pillars<DualFwd> for InterpolatedVolatilityCube<DualFwd> {
fn pillars(&self) -> Option<Vec<(String, &DualFwd)>> {
self.labels.as_ref().map(|labels| {
labels
.iter()
.zip(
self.points
.values()
.flat_map(|t| t.values().flat_map(|m| m.values())),
)
.map(|(label, value)| (label.clone(), value))
.collect()
})
}
fn pillar_labels(&self) -> Option<Vec<String>> {
self.labels.clone()
}
fn put_pillars_on_tape(&mut self) {
for t in self.points.values_mut() {
for m in t.values_mut() {
for value in m.values_mut() {
value.put_on_tape();
}
}
}
}
}
impl ADVolatilityCubeElement for InterpolatedVolatilityCube<DualFwd> {}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use crate::{
ad::dual::DualFwd,
indices::marketindex::MarketIndex,
time::{date::Date, enums::TimeUnit, period::Period},
volatility::{
volatilitycube::VolatilityCube,
volatilityindexing::{F64Key, SmileType, VolatilityType},
},
};
use super::InterpolatedVolatilityCube;
fn build_cube_f64() -> InterpolatedVolatilityCube<f64> {
let mut points = BTreeMap::new();
for (exp_n, exp_unit) in [(6, TimeUnit::Months), (12, TimeUnit::Months)] {
let mut tenor_map = BTreeMap::new();
for (ten_n, ten_unit) in [(1, TimeUnit::Years), (5, TimeUnit::Years)] {
let exp_y = Period::new(exp_n, exp_unit).period_in_year();
let ten_y = Period::new(ten_n, ten_unit).period_in_year();
let smile = BTreeMap::from([
(
F64Key::new(0.02),
0.20 + 0.02 * exp_y + 0.01 * ten_y + 0.001 * 0.02,
),
(
F64Key::new(0.05),
0.20 + 0.02 * exp_y + 0.01 * ten_y + 0.001 * 0.05,
),
]);
tenor_map.insert(Period::new(ten_n, ten_unit), smile);
}
points.insert(Period::new(exp_n, exp_unit), tenor_map);
}
InterpolatedVolatilityCube::new(
Date::new(2025, 1, 1),
MarketIndex::SOFR,
points,
VolatilityType::Black,
SmileType::Strike,
)
}
#[test]
fn cube_interpolates_interior_f64() {
let cube = build_cube_f64();
let vol = cube
.volatility_from_period(
Period::new(9, TimeUnit::Months),
Period::new(3, TimeUnit::Years),
0.035,
)
.expect("cube interpolation should work");
assert!((vol - 0.245_035).abs() < 1e-10, "got {vol}");
}
#[test]
fn cube_interpolates_interior_dualfwd() {
let f64_cube = build_cube_f64();
let mut points = BTreeMap::new();
for (exp, tenor_map) in f64_cube.points.iter() {
let mut dm = BTreeMap::new();
for (tenor, smile) in tenor_map {
let ds: BTreeMap<F64Key, DualFwd> = smile
.iter()
.map(|(k, v)| (k.clone(), DualFwd::from(*v)))
.collect();
dm.insert(*tenor, ds);
}
points.insert(*exp, dm);
}
let cube = InterpolatedVolatilityCube::new(
Date::new(2025, 1, 1),
MarketIndex::SOFR,
points,
VolatilityType::Black,
SmileType::Strike,
);
let vol = cube
.volatility_from_period(
Period::new(9, TimeUnit::Months),
Period::new(3, TimeUnit::Years),
0.035,
)
.expect("cube interpolation DualFwd");
assert!((vol.value() - 0.245_035).abs() < 1e-10);
}
#[test]
fn cube_out_of_grid_returns_error() {
let cube = build_cube_f64();
let vol = cube.volatility_from_period(
Period::new(3, TimeUnit::Months),
Period::new(3, TimeUnit::Years),
0.035,
);
assert!(vol.is_err());
}
}