use implied_vol::{DefaultSpecialFn, ImpliedNormalVolatility, PriceBachelier};
use crate::error::VolSurfError;
use crate::types::{OptionType, Vol};
use crate::validate::{validate_finite, validate_non_negative, validate_positive};
#[derive(Debug)]
pub struct NormalImpliedVol;
impl NormalImpliedVol {
pub fn compute(
option_price: f64,
forward: f64,
strike: f64,
expiry: f64,
option_type: OptionType,
) -> crate::error::Result<Vol> {
validate_non_negative(option_price, "option_price")?;
validate_finite(forward, "forward")?;
validate_finite(strike, "strike")?;
validate_positive(expiry, "expiry")?;
let is_call = matches!(option_type, OptionType::Call);
let iv = ImpliedNormalVolatility::builder()
.option_price(option_price)
.forward(forward)
.strike(strike)
.expiry(expiry)
.is_call(is_call)
.build()
.ok_or_else(|| VolSurfError::InvalidInput {
message: "implied-vol rejected inputs as outside model domain".into(),
})?;
let sigma =
iv.calculate::<DefaultSpecialFn>()
.ok_or_else(|| VolSurfError::NumericalError {
message: "option price is outside the attainable range".into(),
})?;
Ok(Vol(sigma))
}
}
pub fn normal_price(
forward: f64,
strike: f64,
vol: f64,
expiry: f64,
option_type: OptionType,
) -> crate::error::Result<f64> {
validate_finite(forward, "forward")?;
validate_finite(strike, "strike")?;
validate_non_negative(vol, "volatility")?;
validate_non_negative(expiry, "expiry")?;
let is_call = matches!(option_type, OptionType::Call);
let price = PriceBachelier::builder()
.forward(forward)
.strike(strike)
.volatility(vol)
.expiry(expiry)
.is_call(is_call)
.build()
.ok_or_else(|| VolSurfError::InvalidInput {
message: "implied-vol rejected pricing inputs as outside model domain".into(),
})?
.calculate::<DefaultSpecialFn>();
Ok(price)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn normal_price_put_call_parity() {
let (f, k, sigma, t) = (100.0, 110.0, 20.0, 1.0);
let call = normal_price(f, k, sigma, t, OptionType::Call).unwrap();
let put = normal_price(f, k, sigma, t, OptionType::Put).unwrap();
assert_abs_diff_eq!(call - put, f - k, epsilon = 1e-10);
}
#[test]
fn normal_price_zero_vol_itm_call() {
let price = normal_price(100.0, 80.0, 0.0, 1.0, OptionType::Call).unwrap();
assert_abs_diff_eq!(price, 20.0, epsilon = 1e-12);
}
#[test]
fn normal_price_zero_vol_otm_call() {
let price = normal_price(100.0, 120.0, 0.0, 1.0, OptionType::Call).unwrap();
assert_abs_diff_eq!(price, 0.0, epsilon = 1e-12);
}
#[test]
fn normal_price_zero_vol_itm_put() {
let price = normal_price(100.0, 120.0, 0.0, 1.0, OptionType::Put).unwrap();
assert_abs_diff_eq!(price, 20.0, epsilon = 1e-12);
}
#[test]
fn normal_price_zero_vol_otm_put() {
let price = normal_price(100.0, 80.0, 0.0, 1.0, OptionType::Put).unwrap();
assert_abs_diff_eq!(price, 0.0, epsilon = 1e-12);
}
#[test]
fn normal_price_zero_expiry() {
let price = normal_price(100.0, 80.0, 20.0, 0.0, OptionType::Call).unwrap();
assert_abs_diff_eq!(price, 20.0, epsilon = 1e-12);
}
#[test]
fn normal_price_atm_call() {
let (f, k, sigma, t) = (100.0, 100.0, 20.0, 1.0);
let price = normal_price(f, k, sigma, t, OptionType::Call).unwrap();
let expected = sigma * t.sqrt() / (2.0 * std::f64::consts::PI).sqrt();
assert_abs_diff_eq!(price, expected, epsilon = 1e-12);
}
#[test]
fn normal_price_rejects_negative_vol() {
let result = normal_price(100.0, 100.0, -1.0, 1.0, OptionType::Call);
assert!(matches!(result, Err(VolSurfError::InvalidInput { .. })));
}
#[test]
fn normal_price_rejects_negative_expiry() {
let result = normal_price(100.0, 100.0, 20.0, -1.0, OptionType::Call);
assert!(matches!(result, Err(VolSurfError::InvalidInput { .. })));
}
#[test]
fn normal_price_rejects_nan_forward() {
let result = normal_price(f64::NAN, 100.0, 20.0, 1.0, OptionType::Call);
assert!(matches!(result, Err(VolSurfError::InvalidInput { .. })));
}
#[test]
fn normal_price_rejects_inf_strike() {
let result = normal_price(100.0, f64::INFINITY, 20.0, 1.0, OptionType::Call);
assert!(matches!(result, Err(VolSurfError::InvalidInput { .. })));
}
#[test]
fn normal_price_negative_forward() {
let call = normal_price(-1.0, -0.5, 0.5, 1.0, OptionType::Call).unwrap();
let put = normal_price(-1.0, -0.5, 0.5, 1.0, OptionType::Put).unwrap();
assert_abs_diff_eq!(call - put, -1.0 - (-0.5), epsilon = 1e-10);
}
#[test]
fn round_trip_atm_call() {
let (f, k, t, sigma) = (100.0, 100.0, 1.0, 20.0);
let price = normal_price(f, k, sigma, t, OptionType::Call).unwrap();
let iv = NormalImpliedVol::compute(price, f, k, t, OptionType::Call).unwrap();
let reprice = normal_price(f, k, iv.0, t, OptionType::Call).unwrap();
assert_abs_diff_eq!(price, reprice, epsilon = 1e-12);
}
#[test]
fn round_trip_atm_put() {
let (f, k, t, sigma) = (100.0, 100.0, 1.0, 20.0);
let price = normal_price(f, k, sigma, t, OptionType::Put).unwrap();
let iv = NormalImpliedVol::compute(price, f, k, t, OptionType::Put).unwrap();
let reprice = normal_price(f, k, iv.0, t, OptionType::Put).unwrap();
assert_abs_diff_eq!(price, reprice, epsilon = 1e-12);
}
#[test]
fn round_trip_itm_call() {
let (f, k, t, sigma) = (100.0, 80.0, 1.0, 15.0);
let price = normal_price(f, k, sigma, t, OptionType::Call).unwrap();
let iv = NormalImpliedVol::compute(price, f, k, t, OptionType::Call).unwrap();
let reprice = normal_price(f, k, iv.0, t, OptionType::Call).unwrap();
assert_abs_diff_eq!(price, reprice, epsilon = 1e-12);
}
#[test]
fn round_trip_otm_call() {
let (f, k, t, sigma) = (100.0, 120.0, 1.0, 25.0);
let price = normal_price(f, k, sigma, t, OptionType::Call).unwrap();
let iv = NormalImpliedVol::compute(price, f, k, t, OptionType::Call).unwrap();
let reprice = normal_price(f, k, iv.0, t, OptionType::Call).unwrap();
assert_abs_diff_eq!(price, reprice, epsilon = 1e-12);
}
#[test]
fn round_trip_itm_put() {
let (f, k, t, sigma) = (100.0, 120.0, 1.0, 25.0);
let price = normal_price(f, k, sigma, t, OptionType::Put).unwrap();
let iv = NormalImpliedVol::compute(price, f, k, t, OptionType::Put).unwrap();
let reprice = normal_price(f, k, iv.0, t, OptionType::Put).unwrap();
assert_abs_diff_eq!(price, reprice, epsilon = 1e-12);
}
#[test]
fn round_trip_otm_put() {
let (f, k, t, sigma) = (100.0, 80.0, 1.0, 15.0);
let price = normal_price(f, k, sigma, t, OptionType::Put).unwrap();
let iv = NormalImpliedVol::compute(price, f, k, t, OptionType::Put).unwrap();
let reprice = normal_price(f, k, iv.0, t, OptionType::Put).unwrap();
assert_abs_diff_eq!(price, reprice, epsilon = 1e-12);
}
#[test]
fn round_trip_short_expiry() {
let (f, k, t, sigma) = (100.0, 100.0, 0.01, 20.0);
let price = normal_price(f, k, sigma, t, OptionType::Call).unwrap();
let iv = NormalImpliedVol::compute(price, f, k, t, OptionType::Call).unwrap();
let reprice = normal_price(f, k, iv.0, t, OptionType::Call).unwrap();
assert_abs_diff_eq!(price, reprice, epsilon = 1e-12);
}
#[test]
fn round_trip_long_expiry() {
let (f, k, t, sigma) = (100.0, 100.0, 10.0, 20.0);
let price = normal_price(f, k, sigma, t, OptionType::Call).unwrap();
let iv = NormalImpliedVol::compute(price, f, k, t, OptionType::Call).unwrap();
let reprice = normal_price(f, k, iv.0, t, OptionType::Call).unwrap();
assert_abs_diff_eq!(price, reprice, epsilon = 1e-12);
}
#[test]
fn round_trip_high_vol() {
let (f, k, t, sigma) = (100.0, 100.0, 1.0, 80.0);
let price = normal_price(f, k, sigma, t, OptionType::Call).unwrap();
let iv = NormalImpliedVol::compute(price, f, k, t, OptionType::Call).unwrap();
let reprice = normal_price(f, k, iv.0, t, OptionType::Call).unwrap();
assert_abs_diff_eq!(price, reprice, epsilon = 1e-12);
}
#[test]
fn round_trip_negative_forward() {
let (f, k, t, sigma) = (-0.5, -0.3, 1.0, 0.5);
let price = normal_price(f, k, sigma, t, OptionType::Put).unwrap();
let iv = NormalImpliedVol::compute(price, f, k, t, OptionType::Put).unwrap();
let reprice = normal_price(f, k, iv.0, t, OptionType::Put).unwrap();
assert_abs_diff_eq!(price, reprice, epsilon = 1e-12);
}
#[test]
fn round_trip_deep_otm_call() {
let (f, k, t, sigma) = (100.0, 160.0, 1.0, 20.0);
let price = normal_price(f, k, sigma, t, OptionType::Call).unwrap();
assert!(price > 0.0, "deep OTM normal price should be positive");
let iv = NormalImpliedVol::compute(price, f, k, t, OptionType::Call).unwrap();
let reprice = normal_price(f, k, iv.0, t, OptionType::Call).unwrap();
assert_abs_diff_eq!(price, reprice, epsilon = 1e-12);
}
#[test]
fn compute_rejects_negative_price() {
let result = NormalImpliedVol::compute(-1.0, 100.0, 100.0, 1.0, OptionType::Call);
assert!(matches!(result, Err(VolSurfError::InvalidInput { .. })));
}
#[test]
fn compute_rejects_nan_price() {
let result = NormalImpliedVol::compute(f64::NAN, 100.0, 100.0, 1.0, OptionType::Call);
assert!(matches!(result, Err(VolSurfError::InvalidInput { .. })));
}
#[test]
fn compute_rejects_zero_expiry() {
let result = NormalImpliedVol::compute(5.0, 100.0, 100.0, 0.0, OptionType::Call);
assert!(matches!(result, Err(VolSurfError::InvalidInput { .. })));
}
#[test]
fn compute_rejects_negative_expiry() {
let result = NormalImpliedVol::compute(5.0, 100.0, 100.0, -1.0, OptionType::Call);
assert!(matches!(result, Err(VolSurfError::InvalidInput { .. })));
}
#[test]
fn compute_rejects_nan_forward() {
let result = NormalImpliedVol::compute(5.0, f64::NAN, 100.0, 1.0, OptionType::Call);
assert!(matches!(result, Err(VolSurfError::InvalidInput { .. })));
}
#[test]
fn compute_rejects_inf_strike() {
let result = NormalImpliedVol::compute(5.0, 100.0, f64::INFINITY, 1.0, OptionType::Call);
assert!(matches!(result, Err(VolSurfError::InvalidInput { .. })));
}
#[test]
fn compute_price_below_intrinsic() {
let result = NormalImpliedVol::compute(15.0, 100.0, 80.0, 1.0, OptionType::Call);
assert!(result.is_err());
}
#[test]
fn compute_zero_price_otm() {
let iv = NormalImpliedVol::compute(0.0, 100.0, 120.0, 1.0, OptionType::Call).unwrap();
assert_abs_diff_eq!(iv.0, 0.0, epsilon = 1e-10);
}
#[test]
fn compute_zero_price_atm() {
let iv = NormalImpliedVol::compute(0.0, 100.0, 100.0, 1.0, OptionType::Call).unwrap();
assert_abs_diff_eq!(iv.0, 0.0, epsilon = 1e-10);
}
#[test]
fn round_trip_deep_otm_underflow() {
let (f, k, t, sigma) = (100.0, 900.0, 1.0, 20.0);
let price = normal_price(f, k, sigma, t, OptionType::Call).unwrap();
assert!(price < 1e-100, "deep OTM should underflow to near zero");
let iv = NormalImpliedVol::compute(price, f, k, t, OptionType::Call).unwrap();
assert_abs_diff_eq!(iv.0, 0.0, epsilon = 1e-6);
}
#[test]
fn round_trip_small_sigma() {
let (f, k, t, sigma) = (100.0, 100.5, 1.0, 0.01);
let price = normal_price(f, k, sigma, t, OptionType::Call).unwrap();
let iv = NormalImpliedVol::compute(price, f, k, t, OptionType::Call).unwrap();
let reprice = normal_price(f, k, iv.0, t, OptionType::Call).unwrap();
assert_abs_diff_eq!(price, reprice, epsilon = 1e-12);
}
#[test]
fn round_trip_large_negative_rates() {
let (f, k, t, sigma) = (-5.0, -3.0, 2.0, 2.0);
let call = normal_price(f, k, sigma, t, OptionType::Call).unwrap();
let put = normal_price(f, k, sigma, t, OptionType::Put).unwrap();
assert_abs_diff_eq!(call - put, f - k, epsilon = 1e-10);
let iv_call = NormalImpliedVol::compute(call, f, k, t, OptionType::Call).unwrap();
let iv_put = NormalImpliedVol::compute(put, f, k, t, OptionType::Put).unwrap();
assert_abs_diff_eq!(iv_call.0, sigma, epsilon = 1e-10);
assert_abs_diff_eq!(iv_put.0, sigma, epsilon = 1e-10);
}
#[test]
fn round_trip_very_high_vol() {
let (f, k, t, sigma) = (100.0, 100.0, 1.0, 1000.0);
let price = normal_price(f, k, sigma, t, OptionType::Call).unwrap();
assert!(price > 100.0, "very high vol should give very large price");
let iv = NormalImpliedVol::compute(price, f, k, t, OptionType::Call).unwrap();
let reprice = normal_price(f, k, iv.0, t, OptionType::Call).unwrap();
assert_abs_diff_eq!(price, reprice, epsilon = 1e-8);
}
#[test]
fn compute_price_at_intrinsic_itm() {
let iv = NormalImpliedVol::compute(20.0, 100.0, 80.0, 1.0, OptionType::Call).unwrap();
assert_abs_diff_eq!(iv.0, 0.0, epsilon = 1e-10);
}
}