use super::black_scholes::BlackScholes;
use super::error::IVError;
use super::types::IVParams;
#[derive(Debug, Clone)]
pub struct SolverConfig {
pub max_iterations: u32,
pub tolerance: f64,
pub initial_guess: f64,
pub min_iv: f64,
pub max_iv: f64,
pub min_vega: f64,
}
impl Default for SolverConfig {
fn default() -> Self {
Self {
max_iterations: 100,
tolerance: 1e-8,
initial_guess: 0.25,
min_iv: 0.001,
max_iv: 5.0,
min_vega: 1e-10,
}
}
}
impl SolverConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_max_iterations(mut self, max_iterations: u32) -> Self {
self.max_iterations = max_iterations;
self
}
#[must_use]
pub fn with_tolerance(mut self, tolerance: f64) -> Self {
self.tolerance = tolerance;
self
}
#[must_use]
pub fn with_initial_guess(mut self, initial_guess: f64) -> Self {
self.initial_guess = initial_guess;
self
}
#[must_use]
pub fn with_bounds(mut self, min_iv: f64, max_iv: f64) -> Self {
self.min_iv = min_iv;
self.max_iv = max_iv;
self
}
}
fn validate_params(params: &IVParams) -> Result<(), IVError> {
if params.spot <= 0.0 {
return Err(IVError::InvalidParams {
message: format!("spot price must be positive, got {}", params.spot),
});
}
if params.strike <= 0.0 {
return Err(IVError::InvalidParams {
message: format!("strike price must be positive, got {}", params.strike),
});
}
if params.time_to_expiry < 0.0 {
return Err(IVError::InvalidParams {
message: format!(
"time to expiry must be non-negative, got {}",
params.time_to_expiry
),
});
}
const MIN_TIME: f64 = 1.0 / (365.0 * 24.0);
if params.time_to_expiry < MIN_TIME {
return Err(IVError::TimeToExpiryTooSmall {
time_to_expiry: params.time_to_expiry,
min_time: MIN_TIME,
});
}
Ok(())
}
fn smart_initial_guess(params: &IVParams, market_price: f64) -> f64 {
let sqrt_time = params.time_to_expiry.sqrt();
let bs_approx = market_price / (0.4 * params.spot * sqrt_time);
bs_approx.clamp(0.05, 2.0)
}
pub fn solve_iv(
params: &IVParams,
market_price: f64,
config: &SolverConfig,
) -> Result<(f64, u32), IVError> {
validate_params(params)?;
if market_price <= 0.0 {
return Err(IVError::InvalidParams {
message: format!("market price must be positive, got {market_price}"),
});
}
let intrinsic = params.intrinsic_value();
if market_price < intrinsic - config.tolerance {
return Err(IVError::PriceBelowIntrinsic {
price: market_price,
intrinsic,
});
}
let mut iv = if (config.initial_guess - 0.25).abs() < 1e-10 {
smart_initial_guess(params, market_price)
} else {
config.initial_guess
};
iv = iv.clamp(config.min_iv, config.max_iv);
for iteration in 0..config.max_iterations {
let price = BlackScholes::price(params, iv);
let diff = price - market_price;
if diff.abs() < config.tolerance {
if iv < config.min_iv || iv > config.max_iv {
return Err(IVError::VolatilityOutOfBounds {
volatility: iv,
min_bound: config.min_iv,
max_bound: config.max_iv,
});
}
return Ok((iv, iteration + 1));
}
let vega = BlackScholes::vega(params, iv);
if vega.abs() < config.min_vega {
if diff > 0.0 {
iv *= 0.9; } else {
iv *= 1.1; }
} else {
let step = diff / vega;
let damped_step = if step.abs() > 0.5 {
step.signum() * 0.5
} else {
step
};
iv -= damped_step;
}
iv = iv.clamp(config.min_iv, config.max_iv);
}
Err(IVError::ConvergenceFailure {
iterations: config.max_iterations,
last_iv: iv,
})
}
pub fn solve_iv_bisection(
params: &IVParams,
market_price: f64,
config: &SolverConfig,
) -> Result<(f64, u32), IVError> {
validate_params(params)?;
if market_price <= 0.0 {
return Err(IVError::InvalidParams {
message: format!("market price must be positive, got {market_price}"),
});
}
let intrinsic = params.intrinsic_value();
if market_price < intrinsic - config.tolerance {
return Err(IVError::PriceBelowIntrinsic {
price: market_price,
intrinsic,
});
}
let mut low = config.min_iv;
let mut high = config.max_iv;
let price_low = BlackScholes::price(params, low);
let price_high = BlackScholes::price(params, high);
if market_price < price_low || market_price > price_high {
return Err(IVError::VolatilityOutOfBounds {
volatility: if market_price < price_low {
config.min_iv
} else {
config.max_iv
},
min_bound: config.min_iv,
max_bound: config.max_iv,
});
}
for iteration in 0..config.max_iterations {
let mid = (low + high) / 2.0;
let price = BlackScholes::price(params, mid);
let diff = price - market_price;
if diff.abs() < config.tolerance || (high - low) < config.tolerance {
return Ok((mid, iteration + 1));
}
if diff > 0.0 {
high = mid;
} else {
low = mid;
}
}
Err(IVError::ConvergenceFailure {
iterations: config.max_iterations,
last_iv: (low + high) / 2.0,
})
}
#[cfg(test)]
mod tests {
use super::*;
const TOLERANCE: f64 = 1e-4;
#[test]
fn test_solve_iv_atm_call() {
let params = IVParams::call(100.0, 100.0, 0.25, 0.05);
let target_vol = 0.25;
let market_price = BlackScholes::price(¶ms, target_vol);
let config = SolverConfig::default();
let (iv, iterations) = solve_iv(¶ms, market_price, &config).unwrap();
assert!((iv - target_vol).abs() < TOLERANCE);
assert!(iterations < 10);
}
#[test]
fn test_solve_iv_atm_put() {
let params = IVParams::put(100.0, 100.0, 0.25, 0.05);
let target_vol = 0.30;
let market_price = BlackScholes::price(¶ms, target_vol);
let config = SolverConfig::default();
let (iv, _) = solve_iv(¶ms, market_price, &config).unwrap();
assert!((iv - target_vol).abs() < TOLERANCE);
}
#[test]
fn test_solve_iv_itm_call() {
let params = IVParams::call(110.0, 100.0, 0.25, 0.05);
let target_vol = 0.20;
let market_price = BlackScholes::price(¶ms, target_vol);
let config = SolverConfig::default();
let (iv, _) = solve_iv(¶ms, market_price, &config).unwrap();
assert!((iv - target_vol).abs() < TOLERANCE);
}
#[test]
fn test_solve_iv_otm_call() {
let params = IVParams::call(90.0, 100.0, 0.25, 0.05);
let target_vol = 0.35;
let market_price = BlackScholes::price(¶ms, target_vol);
let config = SolverConfig::default();
let (iv, _) = solve_iv(¶ms, market_price, &config).unwrap();
assert!((iv - target_vol).abs() < TOLERANCE);
}
#[test]
fn test_solve_iv_high_volatility() {
let params = IVParams::call(100.0, 100.0, 0.25, 0.0);
let target_vol = 1.5; let market_price = BlackScholes::price(¶ms, target_vol);
let config = SolverConfig::default();
let (iv, _) = solve_iv(¶ms, market_price, &config).unwrap();
assert!((iv - target_vol).abs() < TOLERANCE);
}
#[test]
fn test_solve_iv_low_volatility() {
let params = IVParams::call(100.0, 100.0, 0.25, 0.0);
let target_vol = 0.05; let market_price = BlackScholes::price(¶ms, target_vol);
let config = SolverConfig::default();
let (iv, _) = solve_iv(¶ms, market_price, &config).unwrap();
assert!((iv - target_vol).abs() < TOLERANCE);
}
#[test]
fn test_solve_iv_invalid_spot() {
let params = IVParams::call(-100.0, 100.0, 0.25, 0.05);
let config = SolverConfig::default();
let result = solve_iv(¶ms, 5.0, &config);
assert!(matches!(result, Err(IVError::InvalidParams { .. })));
}
#[test]
fn test_solve_iv_invalid_strike() {
let params = IVParams::call(100.0, 0.0, 0.25, 0.05);
let config = SolverConfig::default();
let result = solve_iv(¶ms, 5.0, &config);
assert!(matches!(result, Err(IVError::InvalidParams { .. })));
}
#[test]
fn test_solve_iv_time_too_small() {
let params = IVParams::call(100.0, 100.0, 0.00001, 0.05);
let config = SolverConfig::default();
let result = solve_iv(¶ms, 5.0, &config);
assert!(matches!(result, Err(IVError::TimeToExpiryTooSmall { .. })));
}
#[test]
fn test_solve_iv_price_below_intrinsic() {
let params = IVParams::call(110.0, 100.0, 0.25, 0.0);
let config = SolverConfig::default();
let result = solve_iv(¶ms, 5.0, &config);
assert!(matches!(result, Err(IVError::PriceBelowIntrinsic { .. })));
}
#[test]
fn test_solve_iv_bisection() {
let params = IVParams::call(100.0, 100.0, 0.25, 0.05);
let target_vol = 0.25;
let market_price = BlackScholes::price(¶ms, target_vol);
let config = SolverConfig::default();
let (iv, _) = solve_iv_bisection(¶ms, market_price, &config).unwrap();
assert!((iv - target_vol).abs() < TOLERANCE);
}
#[test]
fn test_solver_config_builder() {
let config = SolverConfig::new()
.with_max_iterations(50)
.with_tolerance(1e-6)
.with_initial_guess(0.30)
.with_bounds(0.01, 3.0);
assert_eq!(config.max_iterations, 50);
assert!((config.tolerance - 1e-6).abs() < 1e-10);
assert!((config.initial_guess - 0.30).abs() < 1e-10);
assert!((config.min_iv - 0.01).abs() < 1e-10);
assert!((config.max_iv - 3.0).abs() < 1e-10);
}
#[test]
fn test_smart_initial_guess() {
let params = IVParams::call(100.0, 100.0, 0.25, 0.0);
let market_price = BlackScholes::price(¶ms, 0.25);
let guess = smart_initial_guess(¶ms, market_price);
assert!(guess > 0.1 && guess < 0.5);
}
#[test]
fn test_convergence_speed() {
let params = IVParams::call(100.0, 100.0, 0.25, 0.05);
let target_vol = 0.25;
let market_price = BlackScholes::price(¶ms, target_vol);
let config = SolverConfig::default();
let (_, iterations) = solve_iv(¶ms, market_price, &config).unwrap();
assert!(iterations <= 10);
}
#[test]
fn test_various_maturities() {
let target_vol = 0.25;
let config = SolverConfig::default();
for days in [7, 30, 90, 180, 365] {
let time = days as f64 / 365.0;
let params = IVParams::call(100.0, 100.0, time, 0.05);
let market_price = BlackScholes::price(¶ms, target_vol);
let (iv, _) = solve_iv(¶ms, market_price, &config).unwrap();
assert!(
(iv - target_vol).abs() < TOLERANCE,
"Failed for {} days maturity",
days
);
}
}
#[test]
fn test_various_moneyness() {
let target_vol = 0.25;
let config = SolverConfig::default();
for strike in [80, 90, 100, 110, 120] {
let params = IVParams::call(100.0, strike as f64, 0.25, 0.05);
let market_price = BlackScholes::price(¶ms, target_vol);
let (iv, _) = solve_iv(¶ms, market_price, &config).unwrap();
assert!(
(iv - target_vol).abs() < TOLERANCE,
"Failed for strike {}",
strike
);
}
}
}