use crate::derivatives::black_scholes::{price_raw, vega_raw};
use crate::derivatives::types::{validate_bsm_params, BsmParams, OptionType};
use crate::util::error::{require_finite, FinanceError, FinanceResult};
pub fn bsm_implied_vol(
params: BsmParams,
option_type: OptionType,
market_price: f64,
) -> FinanceResult<f64> {
validate_bsm_params(params)?;
require_finite("market_price", market_price)?;
if market_price < 0.0 {
return Err(FinanceError::Unsolvable {
message: "market_price must be non-negative",
});
}
let intrinsic = match option_type {
OptionType::Call => (params.spot - params.strike).max(0.0),
OptionType::Put => (params.strike - params.spot).max(0.0),
};
if market_price + 1e-12 < intrinsic && params.time_years == 0.0 {
return Err(FinanceError::Unsolvable {
message: "market_price below intrinsic",
});
}
if params.time_years == 0.0 {
return Err(FinanceError::Unsolvable {
message: "implied vol undefined at expiry (T=0)",
});
}
solve_implied_vol(
market_price,
|sigma| {
let mut q = params;
q.vol = sigma;
price_raw(q, option_type).unwrap_or(f64::NAN)
},
|sigma| {
let mut q = params;
q.vol = sigma;
vega_raw(q, option_type).unwrap_or(0.0)
},
)
}
pub(crate) fn solve_implied_vol(
market_price: f64,
mut price_at: impl FnMut(f64) -> f64,
mut vega_at: impl FnMut(f64) -> f64,
) -> FinanceResult<f64> {
let lo = 1e-8;
let mut hi = 5.0;
let mut price_hi = price_at(hi);
if !price_hi.is_finite() {
return Err(FinanceError::Unsolvable {
message: "model price non-finite during IV solve",
});
}
let mut expand = 0;
while price_hi < market_price && hi < 50.0 && expand < 10 {
hi *= 2.0;
price_hi = price_at(hi);
expand += 1;
}
let price_lo = price_at(lo);
if !price_lo.is_finite() || !price_hi.is_finite() {
return Err(FinanceError::Unsolvable {
message: "model price non-finite during IV solve",
});
}
if market_price < price_lo - 1e-10 {
return Err(FinanceError::Unsolvable {
message: "market_price below zero-vol price",
});
}
if market_price > price_hi + 1e-8 {
return Err(FinanceError::Unsolvable {
message: "market_price above max-vol price in search range",
});
}
let mut sigma = 0.25_f64.max(lo).min(hi);
for _ in 0..30 {
let price = price_at(sigma);
let diff = price - market_price;
if diff.abs() < 1e-10 {
return Ok(sigma);
}
let vega = vega_at(sigma);
if vega < 1e-14 {
break;
}
let next = sigma - diff / vega;
sigma = next.clamp(lo, hi);
}
let target = market_price;
crate::util::root_find::brent_root(lo, hi, |s| price_at(s) - target, 1e-12, 100)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::derivatives::black_scholes::bsm_price;
#[test]
fn round_trip_atm() {
let p = BsmParams::atm_one_year(100.0, 0.05, 0.22);
let mkt = bsm_price(p, OptionType::Call).unwrap();
let iv = bsm_implied_vol(p, OptionType::Call, mkt).unwrap();
assert!((iv - 0.22).abs() < 1e-6);
}
#[test]
fn put_round_trip() {
let p = BsmParams {
spot: 90.0,
strike: 100.0,
time_years: 0.75,
rate: 0.01,
dividend_yield: 0.02,
vol: 0.35,
};
let mkt = bsm_price(p, OptionType::Put).unwrap();
let iv = bsm_implied_vol(p, OptionType::Put, mkt).unwrap();
assert!((iv - 0.35).abs() < 1e-6);
}
}