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::vanilla_option::EquityOption;
17use crate::core::errors::RustyQLibError;
18
19/// Build an implied vol surface from quoted options. Quotes without a
20/// positive market price or violating arbitrage bounds are skipped (with a
21/// warning); at least one valid quote is required.
22pub fn build_implied_vol_surface(contracts: &[Box<EquityOption>]) -> Result<VolSurface, RustyQLibError> {
23    if contracts.is_empty() {
24        return Err(RustyQLibError::invalid_input("vol surface", "no contracts provided".to_string()));
25    }
26    let reference_date = contracts[0].market.valuation_date;
27    let mut smiles: BTreeMap<NaiveDate, Vec<(f64, f64)>> = BTreeMap::new();
28    let mut skipped = 0usize;
29
30    for option in contracts {
31        let target = option.base.current_price.value();
32        if target <= 0.0 {
33            skipped += 1;
34            continue;
35        }
36        match option.try_imp_vol(target) {
37            Ok(vol) => smiles
38                .entry(option.base.maturity_date)
39                .or_default()
40                .push((option.base.strike_price, vol)),
41            Err(err) => {
42                log::warn!(
43                    "skipping quote {} K={} T={}: {err}",
44                    option.base.symbol, option.base.strike_price, option.base.maturity_date
45                );
46                skipped += 1;
47            }
48        }
49    }
50    if smiles.is_empty() {
51        return Err(RustyQLibError::invalid_input("vol surface", format!("no valid quotes ({skipped} skipped)")));
52    }
53
54    let mut tenors = Vec::new();
55    let mut smile_points = Vec::new();
56    for (maturity, mut points) in smiles {
57        points.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
58        points.dedup_by(|a, b| (a.0 - b.0).abs() < 1e-9);
59        tenors.push(Tenor::Date(maturity));
60        smile_points.push(points);
61    }
62    VolSurface::from_strike_smiles(&tenors, &smile_points, reference_date, DayCountConvention::Act365)
63        .map_err(RustyQLibError::from)
64}