use anyhow::{anyhow, Result};
use roots::find_root_brent;
use statrs::distribution::{ContinuousCDF, Normal};
use super::types::*;
pub fn prepare_points(points: &[MarketDataRow], forward: f64, tte: f64) -> Vec<(f64, f64)> {
use std::collections::HashMap;
let mut x_to_omegas: HashMap<String, Vec<f64>> = HashMap::new();
for point in points {
if point.market_iv <= 0.0 {
continue; }
let x = (point.strike_price / forward).ln(); let omega = point.market_iv * point.market_iv * tte;
let x_key = format!("{:.8}", x); x_to_omegas.entry(x_key).or_default().push(omega);
}
let mut result: Vec<(f64, f64)> = x_to_omegas
.into_iter()
.map(|(x_str, omegas)| {
let x: f64 = x_str.parse().unwrap();
let avg_omega = omegas.iter().sum::<f64>() / omegas.len() as f64;
(x, avg_omega)
})
.collect();
result.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
result
}
pub fn linear_interp(sorted_points: &[(f64, f64)], query_x: f64) -> Option<f64> {
linear_interp_with_config(sorted_points, query_x, true)
}
pub fn linear_interp_with_config(
sorted_points: &[(f64, f64)],
query_x: f64,
allow_extrapolation: bool,
) -> Option<f64> {
if sorted_points.is_empty() {
return None;
}
if sorted_points.len() == 1 {
return Some(sorted_points[0].1);
}
let first_x = sorted_points[0].0;
let last_x = sorted_points[sorted_points.len() - 1].0;
if query_x < first_x {
if !allow_extrapolation {
return None; }
if sorted_points.len() < 2 {
return Some(sorted_points[0].1);
}
let (x1, y1) = sorted_points[0];
let (x2, y2) = sorted_points[1];
let slope = (y2 - y1) / (x2 - x1);
let extrapolated = y1 + slope * (query_x - x1);
return if extrapolated > 0.0 {
Some(extrapolated)
} else {
None
};
}
if query_x > last_x {
if !allow_extrapolation {
return None; }
let n = sorted_points.len();
let (x1, y1) = sorted_points[n - 2];
let (x2, y2) = sorted_points[n - 1];
let slope = (y2 - y1) / (x2 - x1);
let extrapolated = y2 + slope * (query_x - x2);
return if extrapolated > 0.0 {
Some(extrapolated)
} else {
None
};
}
for i in 0..sorted_points.len() - 1 {
let (x1, y1) = sorted_points[i];
let (x2, y2) = sorted_points[i + 1];
if query_x >= x1 && query_x <= x2 {
let t = (query_x - x1) / (x2 - x1);
let interpolated = y1 + t * (y2 - y1);
return Some(interpolated);
}
}
None
}
pub fn compute_atm_iv(points: &[MarketDataRow], forward: f64, tte: f64) -> Result<f64> {
if tte <= 0.0 {
return Err(anyhow!("Time to expiration must be positive, got: {}", tte));
}
let sorted_points = prepare_points(points, forward, tte);
if sorted_points.len() < 2 {
return Err(anyhow!("Insufficient points for ATM IV interpolation"));
}
let omega_atm = linear_interp(&sorted_points, 0.0)
.ok_or_else(|| anyhow!("Failed to interpolate ATM variance"))?;
if omega_atm <= 0.0 {
return Err(anyhow!("Non-positive ATM variance: {}", omega_atm));
}
Ok((omega_atm / tte).sqrt())
}
pub fn bs_delta(x: f64, sigma: f64, tte: f64, is_call: bool, q: f64) -> f64 {
if sigma <= 0.0 || tte <= 0.0 {
return if is_call { 0.0 } else { -1.0 };
}
let d1 = -x / (sigma * tte.sqrt()) + 0.5 * sigma * tte.sqrt();
let normal = Normal::new(0.0, 1.0).unwrap();
let fwd_factor = (-q * tte).exp();
if is_call {
normal.cdf(d1) * fwd_factor
} else {
(normal.cdf(d1) - 1.0) * fwd_factor
}
}
pub fn compute_fixed_delta_iv(
target_delta: f64,
sorted_points: &[(f64, f64)],
tte: f64,
tol: f64,
) -> Result<f64> {
compute_fixed_delta_iv_with_config(target_delta, sorted_points, tte, tol, true, 0.0)
}
pub fn compute_fixed_delta_iv_with_config(
target_delta: f64,
sorted_points: &[(f64, f64)],
tte: f64,
tol: f64,
allow_extrapolation: bool,
q: f64,
) -> Result<f64> {
if sorted_points.is_empty() {
return Err(anyhow!("No points available for delta solving"));
}
let is_call = target_delta > 0.0;
let objective = |x: f64| -> f64 {
let omega = match linear_interp_with_config(sorted_points, x, allow_extrapolation) {
Some(w) if w > 0.0 => w,
_ => {
return if is_call {
if target_delta > 0.0 {
-10.0
} else {
10.0
}
} else if target_delta < 0.0 {
10.0
} else {
-10.0
};
}
};
let sigma = (omega / tte).sqrt();
bs_delta(x, sigma, tte, is_call, q) - target_delta
};
let min_x = sorted_points[0].0;
let max_x = sorted_points[sorted_points.len() - 1].0;
let search_min = min_x - 1.0;
let search_max = max_x + 1.0;
match find_root_brent(search_min, search_max, &objective, &mut tol.clone()) {
Ok(x_solution) => {
let omega =
linear_interp_with_config(sorted_points, x_solution, allow_extrapolation)
.ok_or_else(|| anyhow!("Failed to interpolate at solution x={}", x_solution))?;
if omega <= 0.0 {
return Err(anyhow!("Non-positive variance at solution: {}", omega));
}
Ok((omega / tte).sqrt())
}
Err(_) => Err(anyhow!(
"Root finding failed for target_delta={}",
target_delta
)),
}
}
pub fn compute_all_metrics(
delta_ivs: &[DeltaIv],
atm_iv: f64,
) -> (Vec<DeltaMetrics>, Option<f64>, Option<f64>) {
let mut metrics = Vec::new();
let mut rr_25 = None;
let mut bf_25 = None;
let positive_deltas: Vec<f64> = delta_ivs
.iter()
.filter_map(|div| {
if div.delta > 0.0 {
Some(div.delta)
} else {
None
}
})
.collect();
for &pos_delta in &positive_deltas {
let neg_delta = -pos_delta;
let call_iv = delta_ivs
.iter()
.find(|div| (div.delta - pos_delta).abs() < 1e-10)
.map(|div| div.iv);
let put_iv = delta_ivs
.iter()
.find(|div| (div.delta - neg_delta).abs() < 1e-10)
.map(|div| div.iv);
if let (Some(call_vol), Some(put_vol)) = (call_iv, put_iv) {
let rr = call_vol - put_vol;
let bf = (call_vol + put_vol) / 2.0 - atm_iv;
metrics.push(DeltaMetrics {
delta_level: pos_delta,
risk_reversal: rr,
butterfly: bf,
});
if (pos_delta - 0.25).abs() < 1e-10 {
rr_25 = Some(rr);
bf_25 = Some(bf);
}
}
}
(metrics, rr_25, bf_25)
}
pub fn compute_metrics(delta_ivs: &[DeltaIv], atm_iv: f64) -> (Option<f64>, Option<f64>) {
let (_, rr_25, bf_25) = compute_all_metrics(delta_ivs, atm_iv);
(rr_25, bf_25)
}
fn check_point_coverage(points: &[MarketDataRow], config: &LinearIvConfig) {
let has_calls = points.iter().any(|p| p.option_type == "call");
let has_puts = points.iter().any(|p| p.option_type == "put");
let has_negative_deltas = config.deltas.iter().any(|&d| d < 0.0);
let has_positive_deltas = config.deltas.iter().any(|&d| d > 0.0);
if has_negative_deltas && !has_puts {
eprintln!("Warning: Negative deltas requested but no put options in data. Some delta calculations may fail.");
}
if has_positive_deltas && !has_calls {
eprintln!("Warning: Positive deltas requested but no call options in data. Some delta calculations may fail.");
}
if !has_calls && !has_puts {
eprintln!("Warning: No clear option type classification in data.");
}
}
pub fn build_linear_iv_from_market_data(
points: &[MarketDataRow],
config: &LinearIvConfig,
) -> Result<LinearIvOutput> {
if points.is_empty() {
return Err(anyhow!("No market data provided"));
}
let forward = points[0].underlying_price;
let tte = points[0].years_to_exp;
build_linear_iv(points, forward, tte, config)
}
pub fn build_linear_iv(
points: &[MarketDataRow],
forward: f64,
tte: f64,
config: &LinearIvConfig,
) -> Result<LinearIvOutput> {
if points.len() < config.min_points {
return Err(anyhow!(
"Insufficient points: {} < {}",
points.len(),
config.min_points
));
}
check_point_coverage(points, config);
let atm_iv = compute_atm_iv(points, forward, tte)?;
let sorted_points = prepare_points(points, forward, tte);
let mut delta_ivs = Vec::new();
for &delta in &config.deltas {
match compute_fixed_delta_iv_with_config(
delta,
&sorted_points,
tte,
config.solver_tol,
config.allow_extrapolation,
config.dividend_yield,
) {
Ok(iv) => {
delta_ivs.push(DeltaIv { delta, iv });
}
Err(_) => {
continue;
}
}
}
let (delta_metrics, rr_25, bf_25) = compute_all_metrics(&delta_ivs, atm_iv);
Ok(LinearIvOutput {
atm_iv,
delta_ivs,
rr_25,
bf_25,
delta_metrics,
tte,
})
}