Skip to main content

rustyqlib/equity/
vol_surface.rs

1//! Implied volatility surface construction from quoted options.
2//!
3//! Takes a list of options carrying market prices (`current_price`), solves
4//! each for its Black-Scholes implied vol (robust safeguarded Newton), and
5//! assembles the per-maturity smiles into a canonical
6//! [`crate::core::vols::VolSurface`] on absolute strikes — the same type
7//! the pricers consume, so a built surface can immediately price other
8//! options (including through the Dupire local vol model).
9
10use std::collections::BTreeMap;
11use chrono::NaiveDate;
12
13use crate::core::curves::Tenor;
14use crate::core::daycount::DayCountConvention;
15use crate::core::vols::VolSurface;
16use super::vanila_option::EquityOption;
17
18/// Build an implied vol surface from quoted options. Quotes without a
19/// positive market price or violating arbitrage bounds are skipped (with a
20/// warning); at least one valid quote is required.
21pub fn build_implied_vol_surface(contracts: &[Box<EquityOption>]) -> Result<VolSurface, String> {
22    if contracts.is_empty() {
23        return Err("no contracts provided".to_string());
24    }
25    let reference_date = contracts[0].base.valuation_date;
26    let mut smiles: BTreeMap<NaiveDate, Vec<(f64, f64)>> = BTreeMap::new();
27    let mut skipped = 0usize;
28
29    for option in contracts {
30        let target = option.base.current_price.value();
31        if target <= 0.0 {
32            skipped += 1;
33            continue;
34        }
35        match option.try_imp_vol(target) {
36            Ok(vol) => smiles
37                .entry(option.base.maturity_date)
38                .or_default()
39                .push((option.base.strike_price, vol)),
40            Err(err) => {
41                eprintln!(
42                    "skipping quote {} K={} T={}: {err}",
43                    option.base.symbol, option.base.strike_price, option.base.maturity_date
44                );
45                skipped += 1;
46            }
47        }
48    }
49    if smiles.is_empty() {
50        return Err(format!("no valid quotes ({skipped} skipped)"));
51    }
52
53    let mut tenors = Vec::new();
54    let mut smile_points = Vec::new();
55    for (maturity, mut points) in smiles {
56        points.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
57        points.dedup_by(|a, b| (a.0 - b.0).abs() < 1e-9);
58        tenors.push(Tenor::Date(maturity));
59        smile_points.push(points);
60    }
61    VolSurface::from_strike_smiles(&tenors, &smile_points, reference_date, DayCountConvention::Act365)
62        .map_err(|e| e.to_string())
63}