1use serde::{Deserialize, Serialize};
45use thiserror::Error;
46
47use crate::black_scholes;
48
49pub struct ImpliedVol {
53 pub input: Input,
55 pub output: Result<Output, ImpliedVolError>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct Input {
65 pub price: f32,
67 pub spot: f32,
69 pub strike: f32,
71 pub mat: f32,
73 pub rate: f32,
75 pub div: f32,
77 pub iter: u32,
79 pub prec: f32,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct Output {
89 pub vol: f32,
91 pub iter: u32,
93 pub prec: f32,
95}
96
97#[derive(Error, Debug, Serialize, Deserialize)]
101pub enum ImpliedVolError {
102 #[error("negative spot: {0} - must be positive")]
104 NegativeSpot(f32),
105 #[error("negative strike: {0} - must be positive")]
107 NegativeStrike(f32),
108 #[error("negative maturity: {0} - must be positive")]
110 NegativeMat(f32),
111 #[error("out of bound price: {0} - must be between max(0, S-PV(K)) and spot")]
113 OutOfBoundPrice(f32),
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
120pub enum Method {
121 Newton,
123 Halley,
125}
126
127impl ImpliedVol {
128 pub fn new(input: Input, method: Method) -> ImpliedVol {
129 let output = find_vol(&input, method);
130 ImpliedVol { input, output }
131 }
132}
133
134fn find_vol(input: &Input, method: Method) -> Result<Output, ImpliedVolError> {
135 if input.spot < 0.0 {
136 return Err(ImpliedVolError::NegativeSpot(input.spot));
137 }
138 if input.strike < 0.0 {
139 return Err(ImpliedVolError::NegativeStrike(input.strike));
140 }
141 if input.mat < 0.0 {
142 return Err(ImpliedVolError::NegativeMat(input.mat));
143 }
144
145 let pv_r = (-input.rate * input.mat).exp();
146 let pv_q = (-input.div * input.mat).exp();
147 let min_price = (input.spot * pv_q - input.strike * pv_r).max(0.0);
148 let max_price = input.spot;
149
150 if input.price > max_price || input.price < min_price {
151 return Err(ImpliedVolError::OutOfBoundPrice(input.price));
152 }
153
154 let output = match method {
155 Method::Newton => newton(input),
156 Method::Halley => halley(input),
157 };
158
159 Ok(output)
160}
161
162const VOL_START_MIN: f32 = 1e-10;
163
164fn newton(input: &Input) -> Output {
165 let pv_r = (-input.rate * input.mat).exp();
166 let pv_q = (-input.div * input.mat).exp();
167 let moneyness = (input.spot * pv_q) / (input.strike * pv_r);
168 let vol_start = ((2.0 * moneyness.ln().abs() / input.mat).sqrt()).max(VOL_START_MIN);
169
170 let mut vol = vol_start;
171 let mut iter = 0;
172 let prec = input.prec;
173
174 while iter < input.iter {
175 let calc = black_scholes::call_price_vega(
176 input.spot,
177 input.strike,
178 input.rate,
179 input.mat,
180 vol,
181 input.div,
182 );
183
184 let diff = calc.price - input.price;
185 if diff.abs() < input.prec {
186 break;
187 }
188
189 let f = diff;
191 let f_prime = calc.vega;
192 let step = -f / f_prime;
193 vol += step;
194
195 iter += 1;
196 }
197
198 Output { vol, iter, prec }
199}
200
201fn halley(input: &Input) -> Output {
202 let pv_r = (-input.rate * input.mat).exp();
203 let pv_q = (-input.div * input.mat).exp();
204 let moneyness = (input.spot * pv_q) / (input.strike * pv_r);
205 let vol_start = ((2.0 * moneyness.ln().abs() / input.mat).sqrt()).max(VOL_START_MIN);
206
207 let mut vol = vol_start;
208 let mut iter = 0;
209 let mut prec = 0.0;
210
211 while iter < input.iter {
212 let calc = black_scholes::call_price_vega_voma(
213 input.spot,
214 input.strike,
215 input.rate,
216 input.mat,
217 vol,
218 input.div,
219 );
220
221 let diff = calc.price - input.price;
222 if diff.abs() < input.prec {
223 break;
224 }
225
226 let f = diff;
228 let f_prime = calc.vega;
229 let f_second = calc.voma;
230 let step = -(2.0 * f * f_prime) / (2.0 * f_prime.powi(2) - f * f_second);
231 vol += step;
232
233 iter += 1;
234 prec = diff;
235 }
236
237 Output { vol, iter, prec }
238}
239
240#[cfg(test)]
241mod tests {
242
243 use crate::black_scholes::BlackScholes;
244 use crate::black_scholes::Input as BSInput;
245 use crate::implied_vol::Input as IVInput;
246 use crate::implied_vol::{ImpliedVol, Method};
247
248 #[test]
249 fn test_newton() {
250 let test_data: Vec<BSInput> = vec![
251 BSInput {
252 is_call: true,
253 spot: 135.6,
254 strike: 100.0,
255 mat: 3.2,
256 vol: 0.25,
257 rate: 0.03,
258 div: 0.01,
259 },
260 BSInput {
261 is_call: true,
262 spot: 60.6,
263 strike: 100.0,
264 mat: 3.2,
265 vol: 0.25,
266 rate: 0.03,
267 div: 0.01,
268 },
269 BSInput {
270 is_call: true,
271 spot: 100.0,
272 strike: 100.0,
273 mat: 3.2,
274 vol: 0.35,
275 rate: 0.03,
276 div: 0.00,
277 },
278 BSInput {
279 is_call: true,
280 spot: 100.0,
281 strike: 100.0,
282 mat: 1.0,
283 vol: 0.10,
284 rate: 0.00,
285 div: 0.00,
286 },
287 ];
288
289 test_data.iter().for_each(|input| {
290 let bs_call = BlackScholes::new(input.clone());
291 let bs_vol = bs_call.input.vol;
292
293 let iv_input = IVInput {
294 price: bs_call.output.unwrap().price,
295 spot: input.spot,
296 strike: input.strike,
297 mat: input.mat,
298 rate: input.rate,
299 div: input.div,
300 iter: 10,
301 prec: 1e-6,
302 };
303
304 let iv = ImpliedVol::new(iv_input, Method::Newton);
305 let iv_vol = iv.output.unwrap().vol;
306
307 println!("bs_vol: {}, iv_vol: {}", bs_vol, iv_vol);
308
309 let epsilon = 1e-5;
310 assert!((bs_vol - iv_vol).abs() < epsilon);
311 })
312 }
313
314 #[test]
315 fn test_halley() {
316 let test_data: Vec<BSInput> = vec![
317 BSInput {
318 is_call: true,
319 spot: 135.6,
320 strike: 100.0,
321 mat: 3.2,
322 vol: 0.25,
323 rate: 0.03,
324 div: 0.01,
325 },
326 BSInput {
327 is_call: true,
328 spot: 60.6,
329 strike: 100.0,
330 mat: 3.2,
331 vol: 0.25,
332 rate: 0.03,
333 div: 0.01,
334 },
335 BSInput {
336 is_call: true,
337 spot: 100.0,
338 strike: 100.0,
339 mat: 3.2,
340 vol: 0.35,
341 rate: 0.03,
342 div: 0.00,
343 },
344 BSInput {
345 is_call: true,
346 spot: 100.0,
347 strike: 100.0,
348 mat: 1.0,
349 vol: 0.10,
350 rate: 0.00,
351 div: 0.00,
352 },
353 ];
354
355 test_data.iter().for_each(|input| {
356 let bs_call = BlackScholes::new(input.clone());
357 let bs_vol = bs_call.input.vol;
358
359 let iv_input = IVInput {
360 price: bs_call.output.unwrap().price,
361 spot: input.spot,
362 strike: input.strike,
363 mat: input.mat,
364 rate: input.rate,
365 div: input.div,
366 iter: 10,
367 prec: 1e-6,
368 };
369
370 let iv = ImpliedVol::new(iv_input, Method::Halley);
371 let iv_vol = iv.output.unwrap().vol;
372
373 println!("bs_vol: {}, iv_vol: {}", bs_vol, iv_vol);
374
375 let epsilon = 1e-5;
376 assert!((bs_vol - iv_vol).abs() < epsilon);
377 })
378 }
379}