rustyqlib/equity/
vol_surface.rs1use 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
19pub 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}