mod blackconstantvol;
mod blackvariancecurve;
mod blackvariancesurface;
mod localconstantvol;
mod localvolcurve;
mod localvolsurface;
mod localvoltermstructure;
mod optionlet;
mod swaption;
mod volatilitytype;
pub use blackconstantvol::BlackConstantVol;
pub use blackvariancecurve::{BlackVarianceCurve, BlackVolTimeExtrapolation};
pub use blackvariancesurface::{BlackVarianceSurface, Extrapolation};
pub use localconstantvol::LocalConstantVol;
pub use localvolcurve::LocalVolCurve;
pub use localvolsurface::LocalVolSurface;
pub use localvoltermstructure::LocalVolTermStructure;
pub use optionlet::{ConstantOptionletVolatility, OptionletVolatilityStructure};
pub use swaption::{ConstantSwaptionVolatility, SwaptionVolatilityStructure};
pub use volatilitytype::VolatilityType;
use std::any::Any;
use crate::errors::QlResult;
use crate::termstructures::TermStructure;
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::date::Date;
use crate::time::period::Period;
use crate::types::{Rate, Real, Time, Volatility};
use crate::{fail, require};
pub trait VolatilityTermStructure: TermStructure {
fn business_day_convention(&self) -> BusinessDayConvention;
fn min_strike(&self) -> Rate;
fn max_strike(&self) -> Rate;
fn option_date_from_tenor(&self, period: Period) -> QlResult<Date> {
let Some(calendar) = self.calendar() else {
fail!("no calendar provided for this volatility term structure");
};
Ok(calendar.advance_by_period(
self.reference_date()?,
period,
self.business_day_convention(),
false,
))
}
fn check_strike(&self, strike: Rate, extrapolate: bool) -> QlResult<()> {
if !strike.is_finite() {
fail!("strike ({strike}) must be finite");
}
require!(
extrapolate
|| self.allows_extrapolation()
|| (strike >= self.min_strike() && strike <= self.max_strike()),
"strike ({strike}) is outside the curve domain [{min},{max}]",
min = self.min_strike(),
max = self.max_strike()
);
Ok(())
}
}
pub trait BlackVolTermStructure: VolatilityTermStructure + Any {
fn black_vol_impl(&self, t: Time, strike: Real) -> QlResult<Volatility>;
fn black_variance_impl(&self, t: Time, strike: Real) -> QlResult<Real>;
fn variance_from_vol(&self, t: Time, strike: Real) -> QlResult<Real> {
let vol = self.black_vol_impl(t, strike)?;
ensure_finite_volatility(vol)?;
Ok(vol * vol * t)
}
fn black_vol_date(
&self,
maturity: Date,
strike: Real,
extrapolate: bool,
) -> QlResult<Volatility> {
self.check_range_date(maturity, extrapolate)?;
self.check_strike(strike, extrapolate)?;
let t = self.time_from_reference(maturity)?;
let vol = self.black_vol_impl(t, strike)?;
ensure_finite_volatility(vol)?;
Ok(vol)
}
fn black_vol(&self, maturity: Time, strike: Real, extrapolate: bool) -> QlResult<Volatility> {
self.check_range_time(maturity, extrapolate)?;
self.check_strike(strike, extrapolate)?;
let vol = self.black_vol_impl(maturity, strike)?;
ensure_finite_volatility(vol)?;
Ok(vol)
}
fn black_variance_date(
&self,
maturity: Date,
strike: Real,
extrapolate: bool,
) -> QlResult<Real> {
self.check_range_date(maturity, extrapolate)?;
self.check_strike(strike, extrapolate)?;
let t = self.time_from_reference(maturity)?;
let var = self.black_variance_impl(t, strike)?;
ensure_valid_variance(var)?;
Ok(var)
}
fn black_variance(&self, maturity: Time, strike: Real, extrapolate: bool) -> QlResult<Real> {
self.check_range_time(maturity, extrapolate)?;
self.check_strike(strike, extrapolate)?;
let var = self.black_variance_impl(maturity, strike)?;
ensure_valid_variance(var)?;
Ok(var)
}
fn black_forward_vol_dates(
&self,
date1: Date,
date2: Date,
strike: Real,
extrapolate: bool,
) -> QlResult<Volatility> {
require!(date1 <= date2, "{date1} later than {date2}");
self.check_range_date(date2, extrapolate)?;
let time1 = self.time_from_reference(date1)?;
let time2 = self.time_from_reference(date2)?;
self.black_forward_vol(time1, time2, strike, extrapolate)
}
fn black_forward_vol(
&self,
time1: Time,
time2: Time,
strike: Real,
extrapolate: bool,
) -> QlResult<Volatility> {
if time1 > time2 || !time1.is_finite() || !time2.is_finite() {
fail!("{time1} later than {time2}");
}
self.check_range_time(time2, extrapolate)?;
self.check_strike(strike, extrapolate)?;
if time2 == time1 {
if time1 == 0.0 {
let epsilon = 1.0e-5;
let var = self.black_variance_impl(epsilon, strike)?;
ensure_valid_variance(var)?;
Ok((var / epsilon).sqrt())
} else {
let epsilon = Time::min(1.0e-5, time1);
let var1 = self.black_variance_impl(time1 - epsilon, strike)?;
let var2 = self.black_variance_impl(time1 + epsilon, strike)?;
ensure_non_decreasing(var1, var2)?;
Ok(((var2 - var1) / (2.0 * epsilon)).sqrt())
}
} else {
let var1 = self.black_variance_impl(time1, strike)?;
let var2 = self.black_variance_impl(time2, strike)?;
ensure_non_decreasing(var1, var2)?;
Ok(((var2 - var1) / (time2 - time1)).sqrt())
}
}
fn black_forward_variance_dates(
&self,
date1: Date,
date2: Date,
strike: Real,
extrapolate: bool,
) -> QlResult<Real> {
require!(date1 <= date2, "{date1} later than {date2}");
self.check_range_date(date2, extrapolate)?;
let time1 = self.time_from_reference(date1)?;
let time2 = self.time_from_reference(date2)?;
self.black_forward_variance(time1, time2, strike, extrapolate)
}
fn black_forward_variance(
&self,
time1: Time,
time2: Time,
strike: Real,
extrapolate: bool,
) -> QlResult<Real> {
if time1 > time2 || !time1.is_finite() || !time2.is_finite() {
fail!("{time1} later than {time2}");
}
self.check_range_time(time2, extrapolate)?;
self.check_strike(strike, extrapolate)?;
let v1 = self.black_variance_impl(time1, strike)?;
let v2 = self.black_variance_impl(time2, strike)?;
ensure_non_decreasing(v1, v2)?;
Ok(v2 - v1)
}
}
fn ensure_non_decreasing(var1: Real, var2: Real) -> QlResult<()> {
ensure_valid_variance(var1)?;
ensure_valid_variance(var2)?;
if var2 < var1 {
fail!("variances must be non-decreasing");
}
Ok(())
}
fn ensure_valid_variance(var: Real) -> QlResult<()> {
if !var.is_finite() || var < 0.0 {
fail!("variance ({var}) must be finite and non-negative");
}
Ok(())
}
fn ensure_finite_volatility(vol: Volatility) -> QlResult<()> {
if !vol.is_finite() {
fail!("volatility ({vol}) must be finite");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::patterns::observable::{AsObservable, Observable};
use crate::termstructures::TermStructureBase;
use crate::time::calendars::target::Target;
use crate::time::date::Month;
use crate::time::daycounters::actual360::Actual360;
use crate::time::timeunit::TimeUnit;
struct MockVolCurve {
base: TermStructureBase,
vol: Volatility,
variance_override: Option<fn(Time) -> Real>,
strike_domain: (Rate, Rate),
}
impl MockVolCurve {
fn flat(vol: Volatility) -> MockVolCurve {
MockVolCurve {
base: TermStructureBase::with_reference_date(
Date::new(15, Month::June, 2026),
Some(Target::new()),
Some(Actual360::new()),
),
vol,
variance_override: None,
strike_domain: (Rate::MIN, Rate::MAX),
}
}
}
impl AsObservable for MockVolCurve {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl TermStructure for MockVolCurve {
fn base(&self) -> &TermStructureBase {
&self.base
}
fn max_date(&self) -> Date {
Date::new(15, Month::June, 2036)
}
}
impl VolatilityTermStructure for MockVolCurve {
fn business_day_convention(&self) -> BusinessDayConvention {
BusinessDayConvention::Following
}
fn min_strike(&self) -> Rate {
self.strike_domain.0
}
fn max_strike(&self) -> Rate {
self.strike_domain.1
}
}
impl BlackVolTermStructure for MockVolCurve {
fn black_vol_impl(&self, _t: Time, _strike: Real) -> QlResult<Volatility> {
Ok(self.vol)
}
fn black_variance_impl(&self, t: Time, strike: Real) -> QlResult<Real> {
match self.variance_override {
Some(f) => Ok(f(t)),
None => self.variance_from_vol(t, strike),
}
}
}
#[test]
fn variance_defaults_to_vol_squared_times_time() {
let curve = MockVolCurve::flat(0.25);
let var = curve.black_variance(2.0, 100.0, false).unwrap();
assert!((var - 0.25 * 0.25 * 2.0).abs() < 1e-15);
}
#[test]
fn spot_variance_rejects_invalid_variance() {
let mut curve = MockVolCurve::flat(0.2);
curve.variance_override = Some(|_| Real::INFINITY);
assert!(curve.black_variance(1.0, 100.0, false).is_err());
curve.variance_override = Some(|_| -1.0);
assert!(curve.black_variance(1.0, 100.0, false).is_err());
let maturity = curve.reference_date().unwrap() + 30;
assert!(curve.black_variance_date(maturity, 100.0, false).is_err());
}
#[test]
fn forward_vol_of_a_flat_curve_is_the_flat_vol() {
let curve = MockVolCurve::flat(0.2);
for (t1, t2) in [(0.5, 1.5), (0.0, 0.0), (1.0, 1.0), (0.0, 2.0)] {
let fwd = curve.black_forward_vol(t1, t2, 100.0, false).unwrap();
assert!((fwd - 0.2).abs() < 1e-12, "t1={t1} t2={t2} fwd={fwd}");
}
}
#[test]
fn decreasing_variances_are_errors() {
let mut curve = MockVolCurve::flat(0.2);
curve.variance_override = Some(|t| 0.1 * (10.0 - t));
let err = curve.black_forward_vol(1.0, 2.0, 100.0, false).unwrap_err();
assert!(err.message().contains("non-decreasing"));
let err = curve
.black_forward_variance(1.0, 2.0, 100.0, false)
.unwrap_err();
assert!(err.message().contains("non-decreasing"));
}
#[test]
fn zero_time_forward_vol_rejects_invalid_variance() {
let mut curve = MockVolCurve::flat(0.2);
curve.variance_override = Some(|_| Real::INFINITY);
assert!(curve.black_forward_vol(0.0, 0.0, 100.0, false).is_err());
curve.variance_override = Some(|_| -1.0);
assert!(curve.black_forward_vol(0.0, 0.0, 100.0, false).is_err());
}
#[test]
fn strike_checks_gate_the_curve_domain() {
let mut curve = MockVolCurve::flat(0.2);
curve.strike_domain = (90.0, 110.0);
assert!(curve.black_vol(1.0, 100.0, false).is_ok());
let err = curve.black_vol(1.0, 80.0, false).unwrap_err();
assert!(err.message().contains("outside the curve domain"));
assert!(curve.black_vol(1.0, 80.0, true).is_ok());
curve.enable_extrapolation();
assert!(curve.black_vol(1.0, 80.0, false).is_ok());
curve.disable_extrapolation();
assert!(curve.black_vol(1.0, Rate::NAN, false).is_err());
}
#[test]
fn non_finite_times_and_strikes_are_rejected_even_when_extrapolating() {
let curve = MockVolCurve::flat(0.2);
assert!(curve.black_vol(Time::INFINITY, 100.0, true).is_err());
assert!(
curve
.black_forward_vol(0.5, Time::INFINITY, 100.0, true)
.is_err()
);
assert!(
curve
.black_forward_variance(0.5, Time::INFINITY, 100.0, true)
.is_err()
);
assert!(curve.black_vol(1.0, Real::INFINITY, true).is_err());
assert!(curve.black_vol(1.0, Real::NAN, true).is_err());
}
#[test]
fn variance_from_vol_rejects_non_finite_volatility() {
let curve = MockVolCurve::flat(Volatility::INFINITY);
assert!(curve.black_vol(1.0, 100.0, false).is_err());
assert!(curve.black_variance(1.0, 100.0, false).is_err());
}
#[test]
fn forward_variance_of_a_flat_curve_is_additive() {
let curve = MockVolCurve::flat(0.2);
let fwd = curve
.black_forward_variance(1.0, 3.0, 100.0, false)
.unwrap();
assert!((fwd - 0.04 * 2.0).abs() < 1e-15);
}
#[test]
fn reversed_times_and_dates_are_errors() {
let curve = MockVolCurve::flat(0.2);
let err = curve.black_forward_vol(2.0, 1.0, 100.0, false).unwrap_err();
assert!(err.message().contains("later than"));
let reference = Date::new(15, Month::June, 2026);
let err = curve
.black_forward_variance_dates(reference + 30, reference + 10, 100.0, false)
.unwrap_err();
assert!(err.message().contains("later than"));
}
#[test]
fn range_checks_gate_time_and_date() {
let curve = MockVolCurve::flat(0.2);
assert!(curve.black_vol(-0.5, 100.0, false).is_err());
let before = Date::new(14, Month::June, 2026);
assert!(curve.black_vol_date(before, 100.0, false).is_err());
assert!(curve.black_variance_date(before, 100.0, false).is_err());
}
#[test]
fn option_date_from_tenor_advances_swaption_style() {
let curve = MockVolCurve::flat(0.2);
let expected = Target::new().advance(
Date::new(15, Month::June, 2026),
3,
TimeUnit::Months,
BusinessDayConvention::Following,
false,
);
assert_eq!(
curve
.option_date_from_tenor(Period::new(3, TimeUnit::Months))
.unwrap(),
expected
);
}
}