use crate::errors::IvError;
use crate::math::{d1, d2, npdf};
use crate::models::black_scholes;
use crate::types::{OptionParams, OptionType};
const IV_TOL: f64 = 1e-10_f64;
const IV_LOWER: f64 = 1e-8_f64;
const IV_UPPER: f64 = 100.0_f64;
const MAX_ITER_HALLEY_NEWTON: u32 = 100_u32;
const MAX_ITER_BRENT: u32 = 50_u32;
const VEGA_FLOOR: f64 = 1e-12_f64;
const SQRT_2PI: f64 = 2.506_628_274_631_000_5_f64;
const INIT_GUESS_FLOOR: f64 = 0.001_f64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IvSolver {
Auto,
Halley,
Newton,
Jackel,
Brent,
}
#[inline(always)]
fn intrinsic_value(
spot: f64,
strike: f64,
rate: f64,
div_yield: f64,
time: f64,
option_type: OptionType,
) -> f64 {
let df_q = (-div_yield * time).exp();
let df_r = (-rate * time).exp();
let forward_s = spot * df_q;
let forward_k = strike * df_r;
match option_type {
OptionType::Call => {
if forward_s > forward_k {
forward_s - forward_k
} else {
0.0_f64
}
}
OptionType::Put => {
if forward_k > forward_s {
forward_k - forward_s
} else {
0.0_f64
}
}
}
}
#[inline(always)]
fn corrado_miller_guess(
spot: f64,
strike: f64,
rate: f64,
div_yield: f64,
time: f64,
market_price: f64,
option_type: OptionType,
) -> f64 {
let df_q = (-div_yield * time).exp();
let df_r = (-rate * time).exp();
let forward_s = spot * df_q;
let forward_k = strike * df_r;
let call_price = match option_type {
OptionType::Call => market_price,
OptionType::Put => market_price + forward_s - forward_k,
};
let diff = forward_s - forward_k;
let half_diff = 0.5_f64 * diff;
let mid = 0.5_f64 * (forward_s + forward_k);
if mid <= 0.0_f64 || time <= 0.0_f64 {
return INIT_GUESS_FLOOR;
}
let c_adj = call_price - half_diff;
if c_adj <= 0.0_f64 {
let simple = SQRT_2PI * call_price / (mid * time.sqrt());
return if simple > INIT_GUESS_FLOOR {
simple
} else {
INIT_GUESS_FLOOR
};
}
let inner = c_adj.mul_add(c_adj, -(diff * diff) / core::f64::consts::PI);
let correction = if inner > 0.0_f64 { inner.sqrt() } else { 0.0_f64 };
let sigma = SQRT_2PI / time.sqrt() * (c_adj + correction) / (forward_s + forward_k);
if sigma > INIT_GUESS_FLOOR { sigma } else { INIT_GUESS_FLOOR }
}
#[inline(always)]
fn bs_price_at_vol(params: &OptionParams<f64>, vol: f64) -> Result<f64, IvError> {
let trial = OptionParams {
vol,
..*params
};
black_scholes::price(&trial).map_err(|_| IvError::NoSolution)
}
#[inline(always)]
fn bs_vega(
spot: f64,
strike: f64,
rate: f64,
div_yield: f64,
vol: f64,
time: f64,
) -> (f64, f64, f64) {
let sqrt_t = time.sqrt();
let d1_val = d1(spot, strike, rate, div_yield, vol, time);
let d2_val = d2(d1_val, vol, time);
let pd1 = npdf(d1_val);
let exp_qt = (-div_yield * time).exp();
let vega = spot * exp_qt * pd1 * sqrt_t;
(vega, d1_val, d2_val)
}
fn solve_halley(
params: &OptionParams<f64>,
market_price: f64,
init_vol: f64,
) -> Result<f64, IvError> {
let s = params.spot;
let k = params.strike;
let r = params.rate;
let q = params.div_yield;
let t = params.time;
let mut vol = init_vol;
for _ in 0..MAX_ITER_HALLEY_NEWTON {
let model_price = bs_price_at_vol(params, vol)?;
let residual = model_price - market_price;
let (vega, d1_val, d2_val) = bs_vega(s, k, r, q, vol, t);
if vega.abs() < VEGA_FLOOR {
return Err(IvError::NearZeroVega);
}
let newton_step = residual / vega;
let vomma = vega * d1_val * d2_val / vol;
let halley_denom = 1.0_f64 - 0.5_f64 * newton_step * vomma / vega;
let step = if halley_denom.abs() > 0.1_f64 {
newton_step / halley_denom
} else {
newton_step
};
let new_vol = vol - step;
let new_vol = new_vol.clamp(IV_LOWER, IV_UPPER);
if (new_vol - vol).abs() < IV_TOL {
return Ok(new_vol);
}
vol = new_vol;
}
Err(IvError::MaxIterationsReached {
last_vol: vol,
residual: bs_price_at_vol(params, vol).unwrap_or(f64::NAN) - market_price,
})
}
fn solve_newton(
params: &OptionParams<f64>,
market_price: f64,
init_vol: f64,
) -> Result<f64, IvError> {
let s = params.spot;
let k = params.strike;
let r = params.rate;
let q = params.div_yield;
let t = params.time;
let mut vol = init_vol;
for _ in 0..MAX_ITER_HALLEY_NEWTON {
let model_price = bs_price_at_vol(params, vol)?;
let residual = model_price - market_price;
let (vega, _d1_val, _d2_val) = bs_vega(s, k, r, q, vol, t);
if vega.abs() < VEGA_FLOOR {
return Err(IvError::NearZeroVega);
}
let step = residual / vega;
let new_vol = vol - step;
let new_vol = new_vol.clamp(IV_LOWER, IV_UPPER);
if (new_vol - vol).abs() < IV_TOL {
return Ok(new_vol);
}
vol = new_vol;
}
Err(IvError::MaxIterationsReached {
last_vol: vol,
residual: bs_price_at_vol(params, vol).unwrap_or(f64::NAN) - market_price,
})
}
fn solve_jackel(
params: &OptionParams<f64>,
market_price: f64,
init_vol: f64,
) -> Result<f64, IvError> {
let s = params.spot;
let k = params.strike;
let r = params.rate;
let q = params.div_yield;
let t = params.time;
let df_q = (-q * t).exp();
let df_r = (-r * t).exp();
let forward = s * df_q / df_r;
let x = (forward / k).ln();
let sqrt_t = t.sqrt();
let call_price = match params.option_type {
OptionType::Call => market_price,
OptionType::Put => market_price + s * df_q - k * df_r,
};
let normalized_price = call_price * df_r.recip() / k;
let mut vol = init_vol;
if x.abs() < 0.5_f64 {
let guess = SQRT_2PI * call_price / (s * df_q * sqrt_t);
if guess > INIT_GUESS_FLOOR && guess < IV_UPPER {
vol = guess;
}
} else {
let intrinsic_norm = if x > 0.0_f64 {
forward / k - 1.0_f64
} else {
0.0_f64
};
let time_value_norm = normalized_price - intrinsic_norm;
if time_value_norm > 0.0_f64 {
let eta = (-0.5_f64 * x * x).exp();
let guess_st = if eta > 1e-30_f64 {
SQRT_2PI * time_value_norm / eta
} else {
init_vol * sqrt_t
};
let guess = guess_st / sqrt_t;
if guess > INIT_GUESS_FLOOR && guess < IV_UPPER {
vol = guess;
}
}
}
for _ in 0..MAX_ITER_HALLEY_NEWTON {
let model_price = bs_price_at_vol(params, vol)?;
let residual = model_price - market_price;
if residual.abs() < 1e-14_f64 {
return Ok(vol);
}
let (vega, d1_val, d2_val) = bs_vega(s, k, r, q, vol, t);
if vega.abs() < 1e-30_f64 {
return solve_brent(params, market_price);
}
let newton_step = residual / vega;
let vomma = vega * d1_val * d2_val / vol;
let halley_denom = 1.0_f64 - 0.5_f64 * newton_step * vomma / vega;
let step = if halley_denom.abs() > 0.1_f64 {
newton_step / halley_denom
} else {
newton_step
};
let new_vol = vol - step;
let new_vol = new_vol.clamp(IV_LOWER, IV_UPPER);
if (new_vol - vol).abs() < IV_TOL {
return Ok(new_vol);
}
vol = new_vol;
}
Err(IvError::MaxIterationsReached {
last_vol: vol,
residual: bs_price_at_vol(params, vol).unwrap_or(f64::NAN) - market_price,
})
}
fn solve_brent(
params: &OptionParams<f64>,
market_price: f64,
) -> Result<f64, IvError> {
let mut a = IV_LOWER;
let mut b = IV_UPPER;
let mut fa = bs_price_at_vol(params, a)? - market_price;
let mut fb = bs_price_at_vol(params, b)? - market_price;
if fa * fb > 0.0_f64 {
return Err(IvError::NoSolution);
}
if fa.abs() < fb.abs() {
core::mem::swap(&mut a, &mut b);
core::mem::swap(&mut fa, &mut fb);
}
let mut c = a;
let mut fc = fa;
let mut d = b - a;
let mut mflag = true;
for _ in 0..MAX_ITER_BRENT {
if fb.abs() < 1e-14_f64 {
return Ok(b);
}
if (b - a).abs() < IV_TOL {
return Ok(b);
}
let s = if (fa - fc).abs() > 1e-30_f64 && (fb - fc).abs() > 1e-30_f64 {
let term_a = a * fb * fc / ((fa - fb) * (fa - fc));
let term_b = b * fa * fc / ((fb - fa) * (fb - fc));
let term_c = c * fa * fb / ((fc - fa) * (fc - fb));
term_a + term_b + term_c
} else {
b - fb * (b - a) / (fb - fa)
};
let mid = 0.5_f64 * (a + b);
let cond1 = if a < b {
s < (3.0_f64 * a + b) / 4.0_f64 || s > b
} else {
s > (3.0_f64 * a + b) / 4.0_f64 || s < b
};
let cond2 = mflag && (s - b).abs() >= 0.5_f64 * (b - c).abs();
let cond3 = !mflag && (s - b).abs() >= 0.5_f64 * (c - d).abs();
let cond4 = mflag && (b - c).abs() < IV_TOL;
let cond5 = !mflag && (c - d).abs() < IV_TOL;
let s = if cond1 || cond2 || cond3 || cond4 || cond5 {
mflag = true;
mid
} else {
mflag = false;
s
};
let fs = bs_price_at_vol(params, s)? - market_price;
d = c;
c = b;
fc = fb;
if fa * fs < 0.0_f64 {
b = s;
fb = fs;
} else {
a = s;
fa = fs;
}
if fa.abs() < fb.abs() {
core::mem::swap(&mut a, &mut b);
core::mem::swap(&mut fa, &mut fb);
}
}
Err(IvError::MaxIterationsReached {
last_vol: b,
residual: fb,
})
}
#[inline(always)]
pub fn implied_vol(
params: &OptionParams<f64>,
market_price: f64,
solver: IvSolver,
) -> Result<f64, IvError> {
if market_price <= 0.0_f64 {
return Err(IvError::NoSolution);
}
let s = params.spot;
let k = params.strike;
let r = params.rate;
let q = params.div_yield;
let t = params.time;
if s <= 0.0_f64 || k <= 0.0_f64 || t <= 0.0_f64 {
return Err(IvError::NoSolution);
}
let intrinsic = intrinsic_value(s, k, r, q, t, params.option_type);
if market_price < intrinsic - 1e-10_f64 {
return Err(IvError::BelowIntrinsic { intrinsic });
}
let init_vol = corrado_miller_guess(s, k, r, q, t, market_price, params.option_type);
let result = match solver {
IvSolver::Halley => solve_halley(params, market_price, init_vol),
IvSolver::Newton => solve_newton(params, market_price, init_vol),
IvSolver::Jackel => solve_jackel(params, market_price, init_vol),
IvSolver::Brent => solve_brent(params, market_price),
IvSolver::Auto => {
match solve_halley(params, market_price, init_vol) {
Ok(vol) => Ok(vol),
Err(_) => {
match solve_newton(params, market_price, init_vol) {
Ok(vol) => Ok(vol),
Err(_) => {
match solve_jackel(params, market_price, init_vol) {
Ok(vol) => Ok(vol),
Err(_) => {
solve_brent(params, market_price)
}
}
}
}
}
}
}
};
match result {
Ok(vol) if vol < IV_LOWER => Err(IvError::BoundsExceeded { vol }),
Ok(vol) if vol > IV_UPPER => Err(IvError::BoundsExceeded { vol }),
other => other,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::black_scholes::price;
const STANDARD: f64 = 1e-6_f64;
fn atm_call() -> OptionParams<f64> {
OptionParams {
option_type: OptionType::Call,
spot: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
div_yield: 0.02_f64,
vol: 0.20_f64,
time: 1.0_f64,
}
}
fn atm_put() -> OptionParams<f64> {
OptionParams {
option_type: OptionType::Put,
..atm_call()
}
}
fn assert_iv_roundtrip(
params: &OptionParams<f64>,
solver: IvSolver,
tol: f64,
label: &str,
) {
let target_vol = params.vol;
let market_price = price(params).unwrap();
let recovered = implied_vol(params, market_price, solver).unwrap();
assert!(
(recovered - target_vol).abs() < tol,
"{label}: expected {target_vol}, got {recovered}, diff={}",
(recovered - target_vol).abs()
);
}
#[test]
fn test_iv_golden_atm_call_auto() {
let p = atm_call();
let market_price = price(&p).unwrap();
let iv = implied_vol(&p, market_price, IvSolver::Auto).unwrap();
assert!(
(iv - 0.20_f64).abs() < STANDARD,
"ATM call Auto: expected 0.20, got {iv}"
);
}
#[test]
fn test_iv_golden_otm_call_auto() {
let p = OptionParams {
strike: 110.0_f64,
..atm_call()
};
let market_price = price(&p).unwrap();
let iv = implied_vol(&p, market_price, IvSolver::Auto).unwrap();
assert!(
(iv - 0.20_f64).abs() < STANDARD,
"OTM call K=110 Auto: expected 0.20, got {iv}"
);
}
#[test]
fn test_iv_golden_short_expiry_auto() {
let p = OptionParams {
time: 0.1_f64,
..atm_call()
};
let market_price = price(&p).unwrap();
let iv = implied_vol(&p, market_price, IvSolver::Auto).unwrap();
assert!(
(iv - 0.20_f64).abs() < STANDARD,
"Short expiry T=0.1 Auto: expected 0.20, got {iv}"
);
}
#[test]
fn test_iv_golden_deep_otm_auto() {
let p = OptionParams {
strike: 130.0_f64,
..atm_call()
};
let market_price = price(&p).unwrap();
let iv = implied_vol(&p, market_price, IvSolver::Auto).unwrap();
assert!(
(iv - 0.20_f64).abs() < STANDARD,
"Deep OTM K=130 Auto: expected 0.20, got {iv}"
);
}
#[test]
fn test_iv_golden_deep_itm_auto() {
let p = OptionParams {
strike: 70.0_f64,
..atm_call()
};
let market_price = price(&p).unwrap();
let iv = implied_vol(&p, market_price, IvSolver::Auto).unwrap();
assert!(
(iv - 0.20_f64).abs() < STANDARD,
"Deep ITM K=70 Auto: expected 0.20, got {iv}"
);
}
#[test]
fn test_iv_roundtrip_atm_halley() {
assert_iv_roundtrip(&atm_call(), IvSolver::Halley, STANDARD, "ATM Halley");
}
#[test]
fn test_iv_roundtrip_atm_newton() {
assert_iv_roundtrip(&atm_call(), IvSolver::Newton, STANDARD, "ATM Newton");
}
#[test]
fn test_iv_roundtrip_atm_brent() {
assert_iv_roundtrip(&atm_call(), IvSolver::Brent, STANDARD, "ATM Brent");
}
#[test]
fn test_iv_roundtrip_atm_jackel() {
assert_iv_roundtrip(&atm_call(), IvSolver::Jackel, STANDARD, "ATM Jackel");
}
#[test]
fn test_iv_roundtrip_atm_auto() {
assert_iv_roundtrip(&atm_call(), IvSolver::Auto, STANDARD, "ATM Auto");
}
#[test]
fn test_iv_roundtrip_otm_k110_auto() {
let p = OptionParams {
strike: 110.0_f64,
..atm_call()
};
assert_iv_roundtrip(&p, IvSolver::Auto, STANDARD, "OTM K=110");
}
#[test]
fn test_iv_roundtrip_deep_otm_k130_auto() {
let p = OptionParams {
strike: 130.0_f64,
..atm_call()
};
assert_iv_roundtrip(&p, IvSolver::Auto, STANDARD, "Deep OTM K=130");
}
#[test]
fn test_iv_roundtrip_deep_itm_k70_auto() {
let p = OptionParams {
strike: 70.0_f64,
..atm_call()
};
assert_iv_roundtrip(&p, IvSolver::Auto, STANDARD, "Deep ITM K=70");
}
#[test]
fn test_iv_roundtrip_short_expiry_auto() {
let p = OptionParams {
time: 0.1_f64,
..atm_call()
};
assert_iv_roundtrip(&p, IvSolver::Auto, STANDARD, "Short T=0.1");
}
#[test]
fn test_iv_roundtrip_high_vol_auto() {
let p = OptionParams {
vol: 1.5_f64,
..atm_call()
};
assert_iv_roundtrip(&p, IvSolver::Auto, STANDARD, "High vol 1.5");
}
#[test]
fn test_iv_roundtrip_put_atm_auto() {
assert_iv_roundtrip(&atm_put(), IvSolver::Auto, STANDARD, "Put ATM Auto");
}
#[test]
fn test_iv_roundtrip_put_otm_auto() {
let p = OptionParams {
strike: 90.0_f64,
..atm_put()
};
assert_iv_roundtrip(&p, IvSolver::Auto, STANDARD, "Put OTM K=90");
}
#[test]
fn test_iv_negative_price_returns_no_solution() {
let p = atm_call();
let result = implied_vol(&p, -1.0_f64, IvSolver::Auto);
assert!(
matches!(result, Err(IvError::NoSolution)),
"negative price should return NoSolution, got {result:?}"
);
}
#[test]
fn test_iv_zero_price_returns_no_solution() {
let p = atm_call();
let result = implied_vol(&p, 0.0_f64, IvSolver::Auto);
assert!(
matches!(result, Err(IvError::NoSolution)),
"zero price should return NoSolution, got {result:?}"
);
}
#[test]
fn test_iv_below_intrinsic_returns_error() {
let p = OptionParams {
option_type: OptionType::Call,
spot: 150.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
div_yield: 0.02_f64,
vol: 0.20_f64,
time: 1.0_f64,
};
let intrinsic = intrinsic_value(150.0_f64, 100.0_f64, 0.05_f64, 0.02_f64, 1.0_f64, OptionType::Call);
let below_price = intrinsic - 1.0_f64;
let result = implied_vol(&p, below_price, IvSolver::Auto);
assert!(
matches!(result, Err(IvError::BelowIntrinsic { .. })),
"below-intrinsic price should return BelowIntrinsic, got {result:?}"
);
}
#[test]
fn test_iv_solver_debug() {
let s = IvSolver::Auto;
let debug = format!("{s:?}");
assert!(debug.contains("Auto"));
}
#[test]
fn test_iv_solver_eq() {
assert_eq!(IvSolver::Auto, IvSolver::Auto);
assert_ne!(IvSolver::Auto, IvSolver::Halley);
}
#[test]
fn test_corrado_miller_atm_reasonable() {
let guess = corrado_miller_guess(
100.0_f64, 100.0_f64, 0.05_f64, 0.02_f64, 1.0_f64,
price(&atm_call()).unwrap(),
OptionType::Call,
);
assert!(
(guess - 0.20_f64).abs() < 0.10_f64,
"Corrado-Miller ATM guess: expected ~0.20, got {guess}"
);
}
#[test]
fn test_corrado_miller_otm_positive() {
let p = OptionParams {
strike: 110.0_f64,
..atm_call()
};
let mp = price(&p).unwrap();
let guess = corrado_miller_guess(
100.0_f64, 110.0_f64, 0.05_f64, 0.02_f64, 1.0_f64,
mp, OptionType::Call,
);
assert!(
guess > 0.0_f64,
"Corrado-Miller OTM guess must be positive, got {guess}"
);
}
#[test]
fn test_iv_roundtrip_deep_otm_k130_halley() {
let p = OptionParams {
strike: 130.0_f64,
..atm_call()
};
let market_price = price(&p).unwrap();
let _result = implied_vol(&p, market_price, IvSolver::Halley);
}
#[test]
fn test_iv_roundtrip_deep_otm_k130_brent() {
let p = OptionParams {
strike: 130.0_f64,
..atm_call()
};
assert_iv_roundtrip(&p, IvSolver::Brent, STANDARD, "Deep OTM Brent");
}
#[test]
fn test_iv_roundtrip_deep_itm_k70_brent() {
let p = OptionParams {
strike: 70.0_f64,
..atm_call()
};
assert_iv_roundtrip(&p, IvSolver::Brent, STANDARD, "Deep ITM Brent");
}
#[test]
fn test_iv_roundtrip_low_vol_auto() {
let p = OptionParams {
vol: 0.05_f64,
..atm_call()
};
assert_iv_roundtrip(&p, IvSolver::Auto, STANDARD, "Low vol 0.05");
}
#[test]
fn test_iv_roundtrip_long_expiry_auto() {
let p = OptionParams {
time: 3.0_f64,
..atm_call()
};
assert_iv_roundtrip(&p, IvSolver::Auto, STANDARD, "Long T=3.0");
}
#[test]
fn test_iv_roundtrip_negative_rate_auto() {
let p = OptionParams {
rate: -0.01_f64,
div_yield: 0.0_f64,
..atm_call()
};
assert_iv_roundtrip(&p, IvSolver::Auto, STANDARD, "Negative rate");
}
#[test]
fn test_iv_roundtrip_high_div_yield_auto() {
let p = OptionParams {
div_yield: 0.08_f64,
..atm_call()
};
assert_iv_roundtrip(&p, IvSolver::Auto, STANDARD, "High div q=0.08");
}
}