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