use crate::config::SolverConfig;
use crate::pricing;
use crate::types::{SolverMethod, SolverResult, SolverStatus};
const HOURS_PER_YEAR: f64 = 8760.0;
const TWO_PI: f64 = std::f64::consts::TAU;
fn brenner_subrahmanyam_guess(market_price: f64, f: f64, t: f64, iv_min: f64, iv_max: f64) -> f64 {
if f <= 0.0 || t <= 0.0 {
return f64::midpoint(iv_min, iv_max);
}
let sigma_0 = (TWO_PI / t).sqrt() * (market_price / f);
sigma_0.clamp(iv_min, iv_max)
}
fn brent_solve(
market_price: f64,
f: f64,
k: f64,
t: f64,
r: f64,
is_call: bool,
config: &SolverConfig,
) -> SolverResult {
let objective =
|sigma: f64| -> f64 { pricing::price(f, k, t, sigma, r, is_call) - market_price };
let mut a = config.iv_min;
let mut b = config.iv_max;
let mut fa = objective(a);
let mut fb = objective(b);
if fa * fb > 0.0 {
let best_residual = fa.abs().min(fb.abs());
return SolverResult {
iv: f64::NAN,
method: SolverMethod::Brent,
iterations: 0,
converged: false,
status: SolverStatus::NoBracketInRange,
residual: best_residual,
};
}
if fa.abs() < fb.abs() {
std::mem::swap(&mut a, &mut b);
std::mem::swap(&mut fa, &mut fb);
}
let mut c = a;
let mut fc = fa;
let mut mflag = true;
let mut d = b - a;
for i in 0..config.brent_max_iterations {
if (b - a).abs() < config.iv_tolerance {
return SolverResult {
iv: b,
method: SolverMethod::Brent,
iterations: i + 1,
converged: true,
status: SolverStatus::Converged,
residual: fb.abs(),
};
}
if (b - a).abs() < f64::EPSILON * (a.abs() + b.abs()).max(1.0) {
let converged = fb.abs() < config.price_tolerance;
return SolverResult {
iv: if converged { b } else { f64::NAN },
method: SolverMethod::Brent,
iterations: i + 1,
converged,
status: if converged {
SolverStatus::Converged
} else {
SolverStatus::NotIdentifiable
},
residual: fb.abs(),
};
}
let s = if (fa - fc).abs() > f64::EPSILON && (fb - fc).abs() > f64::EPSILON {
a * fb * fc / ((fa - fb) * (fa - fc))
+ b * fa * fc / ((fb - fa) * (fb - fc))
+ c * fa * fb / ((fc - fa) * (fc - fb))
} else {
b - fb * (b - a) / (fb - fa)
};
let midpoint = f64::midpoint(a, b);
let reject_interpolation = {
let bound1 = 3.0_f64.mul_add(a, b) / 4.0;
let (lo, hi) = if bound1 < b { (bound1, b) } else { (b, bound1) };
s < lo || s > hi
} || (mflag && (s - b).abs() >= (b - c).abs() / 2.0)
|| (!mflag && (s - b).abs() >= (c - d).abs() / 2.0)
|| (mflag && (b - c).abs() < 1e-15)
|| (!mflag && (c - d).abs() < 1e-15);
let s = if reject_interpolation {
mflag = true;
midpoint
} else {
mflag = false;
s
};
let fs = objective(s);
d = c;
c = b;
fc = fb;
if fa * fs < 0.0 {
b = s;
fb = fs;
} else {
a = s;
fa = fs;
}
if fa.abs() < fb.abs() {
std::mem::swap(&mut a, &mut b);
std::mem::swap(&mut fa, &mut fb);
}
}
SolverResult {
iv: f64::NAN,
method: SolverMethod::Brent,
iterations: config.brent_max_iterations,
converged: false,
status: SolverStatus::MaxIterations,
residual: fb.abs(),
}
}
#[must_use]
pub fn solve_iv(
market_price: f64,
f: f64,
k: f64,
t: f64,
r: f64,
is_call: bool,
config: &SolverConfig,
) -> SolverResult {
if !(market_price.is_finite()
&& f.is_finite()
&& k.is_finite()
&& t.is_finite()
&& r.is_finite())
{
return SolverResult {
iv: f64::NAN,
method: SolverMethod::NewtonRaphson,
iterations: 0,
converged: false,
status: SolverStatus::InvalidInput,
residual: f64::NAN,
};
}
let near_expiry_cutoff_years = config.near_expiry_cutoff_hours / HOURS_PER_YEAR;
if t <= 0.0 || t < near_expiry_cutoff_years {
return SolverResult {
iv: f64::NAN,
method: SolverMethod::NewtonRaphson,
iterations: 0,
converged: false,
status: SolverStatus::NearExpiryIntrinsic,
residual: 0.0,
};
}
if market_price <= 0.0 {
return SolverResult {
iv: f64::NAN,
method: SolverMethod::NewtonRaphson,
iterations: 0,
converged: false,
status: SolverStatus::NonPositivePrice,
residual: market_price.abs(),
};
}
let intrinsic = pricing::intrinsic_value(f, k, is_call);
let df = (-r * t).exp();
let discounted_intrinsic = df * intrinsic;
if market_price < discounted_intrinsic {
return SolverResult {
iv: f64::NAN,
method: SolverMethod::NewtonRaphson,
iterations: 0,
converged: false,
status: SolverStatus::BelowIntrinsic,
residual: (discounted_intrinsic - market_price).abs(),
};
}
let mut sigma = brenner_subrahmanyam_guess(market_price, f, t, config.iv_min, config.iv_max);
for i in 0..config.nr_max_iterations {
let model_price = pricing::price(f, k, t, sigma, r, is_call);
let v = pricing::vega(f, k, t, sigma, r);
if v.abs() < config.vega_floor {
break;
}
let diff = model_price - market_price;
let step = diff / v;
if step.abs() < config.iv_tolerance {
let sigma_next = (sigma - step).clamp(config.iv_min, config.iv_max);
let residual = (pricing::price(f, k, t, sigma_next, r, is_call) - market_price).abs();
return SolverResult {
iv: sigma_next,
method: SolverMethod::NewtonRaphson,
iterations: i + 1,
converged: true,
status: SolverStatus::Converged,
residual,
};
}
sigma -= step;
sigma = sigma.clamp(config.iv_min, config.iv_max);
}
brent_solve(market_price, f, k, t, r, is_call, config)
}
#[must_use]
#[allow(clippy::too_many_arguments)] pub fn solve_iv_triple(
bid_price: f64,
mid_price: f64,
ask_price: f64,
f: f64,
k: f64,
t: f64,
r: f64,
is_call: bool,
config: &SolverConfig,
) -> (SolverResult, SolverResult, SolverResult) {
let bid_result = solve_iv(bid_price, f, k, t, r, is_call, config);
let mid_result = solve_iv(mid_price, f, k, t, r, is_call, config);
let ask_result = solve_iv(ask_price, f, k, t, r, is_call, config);
(bid_result, mid_result, ask_result)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pricing::{call_price, put_price};
fn default_config() -> SolverConfig {
SolverConfig::default()
}
#[test]
fn atm_call_converges_nr() {
let config = default_config();
let market_price = call_price(100.0, 100.0, 1.0, 0.20, 0.0);
let result = solve_iv(market_price, 100.0, 100.0, 1.0, 0.0, true, &config);
assert!(result.converged);
assert!((result.iv - 0.20).abs() < 1e-6);
assert_eq!(result.method, SolverMethod::NewtonRaphson);
assert!(result.iterations < 10);
}
#[test]
fn atm_put_converges_nr() {
let config = default_config();
let market_price = put_price(100.0, 100.0, 1.0, 0.20, 0.0);
let result = solve_iv(market_price, 100.0, 100.0, 1.0, 0.0, false, &config);
assert!(result.converged);
assert!((result.iv - 0.20).abs() < 1e-6);
}
#[test]
fn deep_otm_call_converges() {
let config = default_config();
let market_price = call_price(100.0, 200.0, 0.5, 0.80, 0.0);
let result = solve_iv(market_price, 100.0, 200.0, 0.5, 0.0, true, &config);
assert!(result.converged);
assert!((result.iv - 0.80).abs() < 1e-4);
}
#[test]
fn deep_itm_put_converges() {
let config = default_config();
let market_price = put_price(100.0, 50.0, 0.5, 0.30, 0.0);
let result = solve_iv(market_price, 100.0, 50.0, 0.5, 0.0, false, &config);
assert!(result.converged);
assert!((result.iv - 0.30).abs() < 0.01);
}
#[test]
fn near_expiry_returns_nan_with_status() {
let config = default_config();
let result = solve_iv(5.0, 105.0, 100.0, 0.0001, 0.0, true, &config);
assert!(!result.converged);
assert!(result.iv.is_nan());
assert_eq!(result.status, SolverStatus::NearExpiryIntrinsic);
}
#[test]
fn negative_time_value_flagged() {
let config = default_config();
let result = solve_iv(9.0, 110.0, 100.0, 1.0, 0.0, true, &config);
assert!(!result.converged);
assert!(result.iv.is_nan());
assert_eq!(result.status, SolverStatus::BelowIntrinsic);
}
#[test]
fn itm_call_between_discounted_and_undiscounted_intrinsic_accepted() {
let config = default_config();
let f = 110.0_f64;
let k = 100.0_f64;
let t = 0.5_f64;
let r = 0.10_f64;
let sigma_true = 0.05_f64;
let market = call_price(f, k, t, sigma_true, r);
let intrinsic = f - k;
let discounted = intrinsic * (-r * t).exp();
assert!(
market > discounted && market < intrinsic,
"test inputs no longer hit the bug zone: market={market}, df*intr={discounted}, intr={intrinsic}",
);
let result = solve_iv(market, f, k, t, r, true, &config);
assert!(
result.converged,
"feasible ITM call price must converge, got {result:?}",
);
assert!(
(result.iv - sigma_true).abs() < 1e-4,
"expected sigma ~= {sigma_true}, got {}",
result.iv,
);
}
#[test]
fn itm_put_between_discounted_and_undiscounted_intrinsic_accepted() {
let config = default_config();
let f = 100.0_f64;
let k = 110.0_f64;
let t = 0.5_f64;
let r = 0.10_f64;
let sigma_true = 0.05_f64;
let market = put_price(f, k, t, sigma_true, r);
let intrinsic = k - f;
let discounted = intrinsic * (-r * t).exp();
assert!(
market > discounted && market < intrinsic,
"test inputs no longer hit the bug zone: market={market}, df*intr={discounted}, intr={intrinsic}",
);
let result = solve_iv(market, f, k, t, r, false, &config);
assert!(
result.converged,
"feasible ITM put price must converge, got {result:?}",
);
assert!(
(result.iv - sigma_true).abs() < 1e-4,
"expected sigma ~= {sigma_true}, got {}",
result.iv,
);
}
#[test]
fn zero_market_price_returns_nan() {
let config = default_config();
let result = solve_iv(0.0, 100.0, 100.0, 1.0, 0.0, true, &config);
assert!(!result.converged);
assert!(result.iv.is_nan());
assert_eq!(result.status, SolverStatus::NonPositivePrice);
}
#[test]
fn non_finite_inputs_return_invalid_input() {
let config = default_config();
let bad_inputs = [
(f64::NAN, 100.0, 100.0, 1.0, 0.0), (5.0, f64::NAN, 100.0, 1.0, 0.0), (5.0, 100.0, f64::NAN, 1.0, 0.0), (5.0, 100.0, 100.0, f64::INFINITY, 0.0), (5.0, 100.0, 100.0, 1.0, f64::NAN), ];
for (mp, f, k, t, r) in bad_inputs {
let result = solve_iv(mp, f, k, t, r, true, &config);
assert_eq!(
result.status,
SolverStatus::InvalidInput,
"inputs {mp},{f},{k},{t},{r}"
);
assert!(!result.converged);
assert!(result.iv.is_nan());
assert_eq!(result.iterations, 0);
}
}
#[test]
fn brent_no_bracket_returns_nan_iv() {
let market_price = call_price(100.0, 100.0, 1.0, 2.0, 0.0);
let config = SolverConfig::builder()
.iv_min(0.01)
.iv_max(1.0) .build();
let result = solve_iv(market_price, 100.0, 100.0, 1.0, 0.0, true, &config);
assert!(result.iv.is_nan());
assert!(!result.converged);
assert_eq!(result.status, SolverStatus::NoBracketInRange);
}
#[test]
fn brent_exhausts_budget_returns_max_iterations() {
let config = SolverConfig::builder()
.nr_max_iterations(0) .brent_max_iterations(1) .build();
let market = call_price(100.0, 100.0, 1.0, 0.20, 0.0); let result = solve_iv(market, 100.0, 100.0, 1.0, 0.0, true, &config);
assert_eq!(result.status, SolverStatus::MaxIterations);
assert!(!result.converged);
assert!(result.iv.is_nan());
}
#[test]
fn brent_bracket_collapse_returns_not_identifiable() {
let config = SolverConfig::builder()
.nr_max_iterations(0)
.iv_tolerance(0.0)
.price_tolerance(0.0)
.build();
let market = 7.9_f64;
let result = solve_iv(market, 100.0, 100.0, 1.0, 0.0, true, &config);
assert_eq!(result.status, SolverStatus::NotIdentifiable);
assert!(!result.converged);
assert!(result.iv.is_nan());
}
#[test]
fn iv_above_iv_max_returns_nan() {
let mut config = default_config();
config.iv_max = 2.0;
let market_price = call_price(100.0, 100.0, 1.0, 3.0, 0.0);
let result = solve_iv(market_price, 100.0, 100.0, 1.0, 0.0, true, &config);
assert!(result.iv.is_nan());
assert!(!result.converged);
assert_eq!(result.status, SolverStatus::NoBracketInRange);
}
#[test]
fn large_forward_atm_converges() {
let config = default_config();
let f = 100_000.0;
let market = call_price(f, f, 0.25, 0.80, 0.0);
let result = solve_iv(market, f, f, 0.25, 0.0, true, &config);
assert!(result.converged, "large-F ATM should converge: {result:?}");
assert_eq!(result.status, SolverStatus::Converged);
assert!((result.iv - 0.80).abs() < 1e-4, "iv={}", result.iv);
}
#[test]
fn deep_otm_recovers_true_sigma_not_flat_endpoint() {
let config = default_config();
let f = 100.0;
let k = 250.0;
let t = 0.10;
let true_sigma = 0.60;
let market = call_price(f, k, t, true_sigma, 0.0);
let result = solve_iv(market, f, k, t, 0.0, true, &config);
assert!(
result.converged,
"deep-OTM point should converge: {result:?}"
);
assert!(
(result.iv - true_sigma).abs() < 1e-3,
"converged to the wrong sigma: got {}, true {true_sigma}",
result.iv,
);
}
#[test]
fn nonzero_risk_free_rate() {
let config = default_config();
let r = 0.05;
let market_price = call_price(100.0, 100.0, 1.0, 0.25, r);
let result = solve_iv(market_price, 100.0, 100.0, 1.0, r, true, &config);
assert!(result.converged);
assert!((result.iv - 0.25).abs() < 1e-6);
}
#[test]
fn solve_iv_triple_independent() {
let config = default_config();
let bid = call_price(100.0, 100.0, 1.0, 0.18, 0.0);
let ask = call_price(100.0, 100.0, 1.0, 0.22, 0.0);
let mid = call_price(100.0, 100.0, 1.0, 0.20, 0.0);
let (bid_r, mid_r, ask_r) =
solve_iv_triple(bid, mid, ask, 100.0, 100.0, 1.0, 0.0, true, &config);
assert!(bid_r.converged && ask_r.converged && mid_r.converged);
assert!((bid_r.iv - 0.18).abs() < 1e-6);
assert!((ask_r.iv - 0.22).abs() < 1e-6);
assert!((mid_r.iv - 0.20).abs() < 1e-6);
}
#[test]
fn solve_iv_triple_partial_failure() {
let config = default_config();
let ask = call_price(100.0, 100.0, 1.0, 0.22, 0.0);
let mid = call_price(100.0, 100.0, 1.0, 0.20, 0.0);
let (bid_r, mid_r, ask_r) =
solve_iv_triple(0.0, mid, ask, 100.0, 100.0, 1.0, 0.0, true, &config);
assert!(!bid_r.converged);
assert!(ask_r.converged);
assert!(mid_r.converged);
}
}