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::vanila_option::EquityOption;
17
18pub 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}