use implied_vol::{DefaultSpecialFn, ImpliedBlackVolatility, PriceBlackScholes};
use crate::error::VolSurfError;
use crate::types::{OptionType, Vol};
use crate::validate::{validate_non_negative, validate_positive};
#[derive(Debug)]
pub struct BlackImpliedVol;
impl BlackImpliedVol {
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_positive(forward, "forward")?;
validate_positive(strike, "strike")?;
validate_positive(expiry, "expiry")?;
let is_call = matches!(option_type, OptionType::Call);
let iv = ImpliedBlackVolatility::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 black_price(
forward: f64,
strike: f64,
vol: f64,
expiry: f64,
option_type: OptionType,
) -> crate::error::Result<f64> {
validate_positive(forward, "forward")?;
validate_positive(strike, "strike")?;
validate_non_negative(vol, "volatility")?;
validate_non_negative(expiry, "expiry")?;
let is_call = matches!(option_type, OptionType::Call);
let price = PriceBlackScholes::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 round_trip_atm_call() {
let (f, k, t, sigma) = (100.0, 100.0, 1.0, 0.20);
let price = black_price(f, k, sigma, t, OptionType::Call).unwrap();
let iv = BlackImpliedVol::compute(price, f, k, t, OptionType::Call).unwrap();
let reprice = black_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, 0.20);
let price = black_price(f, k, sigma, t, OptionType::Put).unwrap();
let iv = BlackImpliedVol::compute(price, f, k, t, OptionType::Put).unwrap();
let reprice = black_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, 0.25);
let price = black_price(f, k, sigma, t, OptionType::Call).unwrap();
let iv = BlackImpliedVol::compute(price, f, k, t, OptionType::Call).unwrap();
let reprice = black_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, 0.30);
let price = black_price(f, k, sigma, t, OptionType::Call).unwrap();
let iv = BlackImpliedVol::compute(price, f, k, t, OptionType::Call).unwrap();
let reprice = black_price(f, k, iv.0, t, OptionType::Call).unwrap();
assert_abs_diff_eq!(price, reprice, epsilon = 1e-12);
}
#[test]
fn round_trip_deep_otm_call() {
let (f, k, t, sigma) = (100.0, 200.0, 1.0, 0.20);
let price = black_price(f, k, sigma, t, OptionType::Call).unwrap();
assert!(price > 0.0, "deep OTM price should be positive");
let iv = BlackImpliedVol::compute(price, f, k, t, OptionType::Call).unwrap();
let reprice = black_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, 1.0);
let price = black_price(f, k, sigma, t, OptionType::Call).unwrap();
let iv = BlackImpliedVol::compute(price, f, k, t, OptionType::Call).unwrap();
let reprice = black_price(f, k, iv.0, t, OptionType::Call).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, 0.20);
let price = black_price(f, k, sigma, t, OptionType::Call).unwrap();
let iv = BlackImpliedVol::compute(price, f, k, t, OptionType::Call).unwrap();
let reprice = black_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, 0.20);
let price = black_price(f, k, sigma, t, OptionType::Call).unwrap();
let iv = BlackImpliedVol::compute(price, f, k, t, OptionType::Call).unwrap();
let reprice = black_price(f, k, iv.0, t, OptionType::Call).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, 0.25);
let price = black_price(f, k, sigma, t, OptionType::Put).unwrap();
let iv = BlackImpliedVol::compute(price, f, k, t, OptionType::Put).unwrap();
let reprice = black_price(f, k, iv.0, t, OptionType::Put).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, 0.30);
let price = black_price(f, k, sigma, t, OptionType::Put).unwrap();
let iv = BlackImpliedVol::compute(price, f, k, t, OptionType::Put).unwrap();
let reprice = black_price(f, k, iv.0, t, OptionType::Put).unwrap();
assert_abs_diff_eq!(price, reprice, epsilon = 1e-12);
}
#[test]
fn black_price_call_put_parity() {
let (f, k, t, sigma) = (100.0, 110.0, 1.0, 0.25);
let call = black_price(f, k, sigma, t, OptionType::Call).unwrap();
let put = black_price(f, k, sigma, t, OptionType::Put).unwrap();
assert_abs_diff_eq!(call - put, f - k, epsilon = 1e-10);
}
#[test]
fn black_price_zero_vol_call() {
let price = black_price(100.0, 80.0, 0.0, 1.0, OptionType::Call).unwrap();
assert_abs_diff_eq!(price, 20.0, epsilon = 1e-12);
}
#[test]
fn black_price_zero_vol_otm_call() {
let price = black_price(100.0, 120.0, 0.0, 1.0, OptionType::Call).unwrap();
assert_abs_diff_eq!(price, 0.0, epsilon = 1e-12);
}
#[test]
fn black_price_zero_expiry() {
let price = black_price(100.0, 80.0, 0.20, 0.0, OptionType::Call).unwrap();
assert_abs_diff_eq!(price, 20.0, epsilon = 1e-12);
}
#[test]
fn compute_rejects_negative_price() {
let result = BlackImpliedVol::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 = BlackImpliedVol::compute(f64::NAN, 100.0, 100.0, 1.0, OptionType::Call);
assert!(matches!(result, Err(VolSurfError::InvalidInput { .. })));
}
#[test]
fn compute_rejects_zero_forward() {
let result = BlackImpliedVol::compute(5.0, 0.0, 100.0, 1.0, OptionType::Call);
assert!(matches!(result, Err(VolSurfError::InvalidInput { .. })));
}
#[test]
fn compute_rejects_negative_forward() {
let result = BlackImpliedVol::compute(5.0, -1.0, 100.0, 1.0, OptionType::Call);
assert!(matches!(result, Err(VolSurfError::InvalidInput { .. })));
}
#[test]
fn compute_rejects_infinite_forward() {
let result = BlackImpliedVol::compute(5.0, f64::INFINITY, 100.0, 1.0, OptionType::Call);
assert!(matches!(result, Err(VolSurfError::InvalidInput { .. })));
}
#[test]
fn compute_rejects_zero_strike() {
let result = BlackImpliedVol::compute(5.0, 100.0, 0.0, 1.0, OptionType::Call);
assert!(matches!(result, Err(VolSurfError::InvalidInput { .. })));
}
#[test]
fn compute_rejects_zero_expiry() {
let result = BlackImpliedVol::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 = BlackImpliedVol::compute(5.0, 100.0, 100.0, -1.0, OptionType::Call);
assert!(matches!(result, Err(VolSurfError::InvalidInput { .. })));
}
#[test]
fn compute_rejects_price_above_forward() {
let result = BlackImpliedVol::compute(150.0, 100.0, 100.0, 1.0, OptionType::Call);
assert!(matches!(result, Err(VolSurfError::NumericalError { .. })));
}
#[test]
fn black_price_rejects_negative_vol() {
let result = black_price(100.0, 100.0, -0.1, 1.0, OptionType::Call);
assert!(matches!(result, Err(VolSurfError::InvalidInput { .. })));
}
#[test]
fn black_price_rejects_negative_expiry() {
let result = black_price(100.0, 100.0, 0.2, -1.0, OptionType::Call);
assert!(matches!(result, Err(VolSurfError::InvalidInput { .. })));
}
#[test]
fn black_price_rejects_zero_forward() {
let result = black_price(0.0, 100.0, 0.2, 1.0, OptionType::Call);
assert!(matches!(result, Err(VolSurfError::InvalidInput { .. })));
}
#[test]
fn black_price_rejects_zero_strike() {
let result = black_price(100.0, 0.0, 0.0, 1.0, OptionType::Call);
assert!(matches!(result, Err(VolSurfError::InvalidInput { .. })));
}
#[test]
fn black_price_rejects_zero_strike_with_zero_vol() {
let result = black_price(100.0, 0.0, 0.0, 1.0, OptionType::Put);
assert!(matches!(result, Err(VolSurfError::InvalidInput { .. })));
}
#[test]
fn compute_rejects_infinite_strike() {
let result = BlackImpliedVol::compute(5.0, 100.0, f64::INFINITY, 1.0, OptionType::Call);
assert!(matches!(result, Err(VolSurfError::InvalidInput { .. })));
}
#[test]
fn compute_rejects_nan_strike() {
let result = BlackImpliedVol::compute(5.0, 100.0, f64::NAN, 1.0, OptionType::Call);
assert!(matches!(result, Err(VolSurfError::InvalidInput { .. })));
}
#[test]
fn black_price_rejects_infinite_strike() {
let result = black_price(100.0, f64::INFINITY, 0.2, 1.0, OptionType::Call);
assert!(matches!(result, Err(VolSurfError::InvalidInput { .. })));
}
#[test]
fn black_price_rejects_nan_strike() {
let result = black_price(100.0, f64::NAN, 0.2, 1.0, OptionType::Call);
assert!(matches!(result, Err(VolSurfError::InvalidInput { .. })));
}
#[test]
fn black_price_zero_vol_itm_put() {
let price = black_price(100.0, 120.0, 0.0, 1.0, OptionType::Put).unwrap();
assert_abs_diff_eq!(price, 20.0, epsilon = 1e-12);
}
#[test]
fn black_price_zero_vol_otm_put() {
let price = black_price(100.0, 80.0, 0.0, 1.0, OptionType::Put).unwrap();
assert_abs_diff_eq!(price, 0.0, epsilon = 1e-12);
}
#[test]
fn black_price_zero_vol_atm_put() {
let price = black_price(100.0, 100.0, 0.0, 1.0, OptionType::Put).unwrap();
assert_abs_diff_eq!(price, 0.0, epsilon = 1e-12);
}
#[test]
fn near_intrinsic_iv_stability() {
let forward = 100.0;
let strike = 80.0;
let expiry = 1.0;
let price = 20.01;
let iv =
BlackImpliedVol::compute(price, forward, strike, expiry, OptionType::Call).unwrap();
assert!(
iv.0 > 0.0,
"IV should be positive for price above intrinsic"
);
assert!(iv.0.is_finite(), "IV should be finite");
let reprice = black_price(forward, strike, iv.0, expiry, OptionType::Call).unwrap();
assert_abs_diff_eq!(reprice, price, epsilon = 1e-8);
}
#[test]
fn round_trip_extreme_otm_call() {
let (f, k, t, sigma) = (100.0, 1000.0, 1.0, 0.80);
let price = black_price(f, k, sigma, t, OptionType::Call).unwrap();
assert!(price > 0.0);
let iv = BlackImpliedVol::compute(price, f, k, t, OptionType::Call).unwrap();
let reprice = black_price(f, k, iv.0, t, OptionType::Call).unwrap();
assert_abs_diff_eq!(price, reprice, epsilon = 1e-12);
}
#[test]
fn round_trip_extreme_itm_call() {
let (f, k, t, sigma) = (100.0, 0.1, 1.0, 0.20);
let price = black_price(f, k, sigma, t, OptionType::Call).unwrap();
let iv = BlackImpliedVol::compute(price, f, k, t, OptionType::Call).unwrap();
let reprice = black_price(f, k, iv.0, t, OptionType::Call).unwrap();
assert_abs_diff_eq!(price, reprice, epsilon = 1e-12);
}
#[test]
fn compute_near_maximum_price() {
let iv = BlackImpliedVol::compute(99.99, 100.0, 100.0, 1.0, OptionType::Call).unwrap();
assert!(iv.0 > 5.0, "near-max price should yield very high vol");
assert!(iv.0.is_finite());
}
#[test]
fn compute_rejects_put_price_above_strike() {
let result = BlackImpliedVol::compute(150.0, 100.0, 100.0, 1.0, OptionType::Put);
assert!(matches!(result, Err(VolSurfError::NumericalError { .. })));
}
#[test]
fn round_trip_very_high_vol() {
let (f, k, t, sigma) = (100.0, 100.0, 1.0, 5.0);
let price = black_price(f, k, sigma, t, OptionType::Call).unwrap();
let iv = BlackImpliedVol::compute(price, f, k, t, OptionType::Call).unwrap();
let reprice = black_price(f, k, iv.0, t, OptionType::Call).unwrap();
assert_abs_diff_eq!(price, reprice, epsilon = 1e-10);
}
}