finance_solution/derivatives/
implied_vol.rs1use crate::derivatives::black_scholes::{price_raw, vega_raw};
48use crate::derivatives::types::{validate_bsm_params, BsmParams, OptionType};
49use crate::util::error::{require_finite, FinanceError, FinanceResult};
50
51pub fn bsm_implied_vol(
58 params: BsmParams,
59 option_type: OptionType,
60 market_price: f64,
61) -> FinanceResult<f64> {
62 validate_bsm_params(params)?;
63 require_finite("market_price", market_price)?;
64 if market_price < 0.0 {
65 return Err(FinanceError::Unsolvable {
66 message: "market_price must be non-negative",
67 });
68 }
69
70 let mut p = params;
71 let intrinsic = match option_type {
72 OptionType::Call => (p.spot - p.strike).max(0.0),
73 OptionType::Put => (p.strike - p.spot).max(0.0),
74 };
75 if market_price + 1e-12 < intrinsic && p.time_years == 0.0 {
76 return Err(FinanceError::Unsolvable {
77 message: "market_price below intrinsic",
78 });
79 }
80
81 if p.time_years == 0.0 {
82 return Err(FinanceError::Unsolvable {
83 message: "implied vol undefined at expiry (T=0)",
84 });
85 }
86
87 let mut lo = 1e-8;
88 let mut hi = 5.0;
89 p.vol = hi;
90 let mut price_hi = price_raw(p, option_type)?;
91 let mut expand = 0;
92 while price_hi < market_price && hi < 50.0 && expand < 10 {
93 hi *= 2.0;
94 p.vol = hi;
95 price_hi = price_raw(p, option_type)?;
96 expand += 1;
97 }
98 p.vol = lo;
99 let price_lo = price_raw(p, option_type)?;
100 if market_price < price_lo - 1e-10 {
101 return Err(FinanceError::Unsolvable {
102 message: "market_price below zero-vol price",
103 });
104 }
105 if market_price > price_hi + 1e-8 {
106 return Err(FinanceError::Unsolvable {
107 message: "market_price above max-vol price in search range",
108 });
109 }
110
111 let mut sigma = 0.25_f64.max(lo).min(hi);
112 for _ in 0..30 {
113 p.vol = sigma;
114 let price = price_raw(p, option_type)?;
115 let diff = price - market_price;
116 if diff.abs() < 1e-10 {
117 return Ok(sigma);
118 }
119 let vega = vega_raw(p, option_type)?;
120 if vega < 1e-14 {
121 break;
122 }
123 let next = sigma - diff / vega;
124 sigma = next.clamp(lo, hi);
125 }
126
127 for _ in 0..80 {
128 let mid = 0.5 * (lo + hi);
129 p.vol = mid;
130 let price = price_raw(p, option_type)?;
131 if (price - market_price).abs() < 1e-10 {
132 return Ok(mid);
133 }
134 if price > market_price {
135 hi = mid;
136 } else {
137 lo = mid;
138 }
139 }
140 Ok(0.5 * (lo + hi))
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::derivatives::black_scholes::bsm_price;
147
148 #[test]
149 fn round_trip_atm() {
150 let p = BsmParams::atm_one_year(100.0, 0.05, 0.22);
151 let mkt = bsm_price(p, OptionType::Call).unwrap();
152 let iv = bsm_implied_vol(p, OptionType::Call, mkt).unwrap();
153 assert!((iv - 0.22).abs() < 1e-6);
154 }
155
156 #[test]
157 fn put_round_trip() {
158 let p = BsmParams {
159 spot: 90.0,
160 strike: 100.0,
161 time_years: 0.75,
162 rate: 0.01,
163 dividend_yield: 0.02,
164 vol: 0.35,
165 };
166 let mkt = bsm_price(p, OptionType::Put).unwrap();
167 let iv = bsm_implied_vol(p, OptionType::Put, mkt).unwrap();
168 assert!((iv - 0.35).abs() < 1e-6);
169 }
170}