use crate::error::VolatilityError;
use crate::kernels::calculate_d_values;
use crate::pricing::OptionPricing;
use optionstratlib_core::model::decimal::{
d_add, d_div, d_mul, d_sqrt, d_sub, decimal_to_f64, f64_to_decimal,
};
use optionstratlib_core::model::{Options, Positive, Side};
use rust_decimal::Decimal;
pub(crate) const IV_BRACKET_MAX: Positive = Positive::FIVE;
const IV_STEP_TOLERANCE: Decimal = rust_decimal_macros::dec!(0.0000000000001);
const IV_GEOMETRIC_SPLIT: Decimal = Decimal::from_parts(4, 0, 0, false, 0);
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum IvOutcome {
Found(Positive),
BelowBracket,
AboveBracket,
NoConvergence(Positive),
}
pub(crate) fn solve_implied_volatility<F>(
low: Positive,
high: Positive,
guess: Positive,
max_iterations: u32,
mut evaluate: F,
) -> Result<IvOutcome, VolatilityError>
where
F: FnMut(Positive, bool) -> Result<(Decimal, Decimal), VolatilityError>,
{
let op = "volatility::iv_solve";
let (residual_low, _) = evaluate(low, false)?;
if residual_low >= Decimal::ZERO {
return Ok(IvOutcome::BelowBracket);
}
let (residual_high, _) = evaluate(high, false)?;
if residual_high.is_zero() {
return Ok(IvOutcome::Found(high));
}
if residual_high < Decimal::ZERO {
return Ok(IvOutcome::AboveBracket);
}
let (mut low, mut high) = (low.to_dec(), high.to_dec());
let midpoint = |low: Decimal, high: Decimal| -> Result<Decimal, VolatilityError> {
if high > d_mul(low, IV_GEOMETRIC_SPLIT, op)?
&& let Ok(geometric) = d_sqrt(d_mul(low, high, op)?, op)
{
return Ok(geometric);
}
Ok(d_div(d_add(low, high, op)?, Decimal::TWO, op)?)
};
let mut sigma = if guess.to_dec() > low && guess.to_dec() < high {
guess.to_dec()
} else {
midpoint(low, high)?
};
for _ in 0..max_iterations {
let current = Positive::new_decimal(sigma)?;
let (residual, slope) = evaluate(current, true)?;
if residual.is_zero() {
return Ok(IvOutcome::Found(current));
}
if residual < Decimal::ZERO {
low = sigma;
} else {
high = sigma;
}
let newton = if slope > Decimal::ZERO {
d_div(residual, slope, op)
.and_then(|step| d_sub(sigma, step, op))
.ok()
.filter(|next| *next > low && *next < high)
} else {
None
};
let next = match newton {
Some(next) => next,
None => midpoint(low, high)?,
};
let moved = d_sub(next, sigma, op)?.abs();
sigma = next;
if moved <= d_mul(IV_STEP_TOLERANCE, sigma, op)? {
return Ok(IvOutcome::Found(Positive::new_decimal(sigma)?));
}
}
Ok(IvOutcome::NoConvergence(Positive::new_decimal(sigma)?))
}
pub(crate) fn iv_initial_guess(option: &Options, price: Decimal) -> Option<Positive> {
let years = decimal_to_f64(option.time_to_expiration().ok()?.to_dec()).ok()?;
let spot = decimal_to_f64(option.underlying_price.to_dec()).ok()?;
let price = decimal_to_f64(price.abs()).ok()?;
if years <= 0.0 || spot <= 0.0 {
return None;
}
let guess = (2.0 * std::f64::consts::PI / years).sqrt() * price / spot; Positive::new(guess).ok()
}
pub(crate) fn long_residual(
option: &Options,
target: Decimal,
sigma: Positive,
with_slope: bool,
) -> Result<(Decimal, Decimal), VolatilityError> {
let mut candidate = option.clone();
candidate.side = Side::Long;
candidate.quantity = Positive::ONE;
candidate.contract_size = Positive::ONE;
candidate.implied_volatility = sigma;
let price = candidate.calculate_price_black_scholes()?;
let residual = d_sub(price, target, "pricing::iv_residual")?;
if !with_slope {
return Ok((residual, Decimal::ZERO));
}
Ok((residual, vega_slope(&candidate)))
}
fn vega_slope(option: &Options) -> Decimal {
let slope = || -> Option<Decimal> {
let (d1, _) = calculate_d_values(option).ok()?;
let d1 = decimal_to_f64(d1).ok()?;
let spot = decimal_to_f64(option.underlying_price.to_dec()).ok()?;
let yield_rate = decimal_to_f64(option.dividend_yield.to_dec()).ok()?;
let years = decimal_to_f64(option.time_to_expiration().ok()?.to_dec()).ok()?;
let vega = spot * (-yield_rate * years).exp() * (-0.5 * d1 * d1).exp() * years.sqrt() / (2.0 * std::f64::consts::PI).sqrt(); f64_to_decimal(vega).ok()
};
slope()
.filter(|v| *v > Decimal::ZERO)
.unwrap_or(Decimal::ZERO)
}