Skip to main content

option_pricing/
implied_vol.rs

1//! # Implied Volatility
2//! This module provides the implied volatility calculation for a given option price.
3//!
4//! ## Example
5//! ```
6//! use crate::black_scholes::BlackScholes;
7//! use crate::black_scholes::Input as BSInput;
8//! use crate::implied_vol::Input as IVInput;
9//! use crate::implied_vol::{ImpliedVol, Method};
10//!
11//! let input = BSInput {
12//!     is_call: true,
13//!     spot: 135.6,
14//!     strike: 100.0,
15//!     mat: 3.2,
16//!     vol: 0.25,
17//!     rate: 0.03,
18//!     div: 0.01,
19//! };
20//!
21//! let iv_input = IVInput {
22//!     price: bs_call.output.unwrap().price,
23//!     spot: input.spot,
24//!     strike: input.strike,
25//!     mat: input.mat,
26//!     rate: input.rate,
27//!     div: input.div,
28//!     iter: 10,
29//!     prec: 1e-5,
30//! };
31//! let iv = ImpliedVol::new(iv_input, Method::Halley);
32//! let iv_vol = iv.output.unwrap().vol;
33//!
34//! let bs_call = BlackScholes::new(input.clone());
35//! let bs_vol = bs_call.input.vol;
36//!
37//! println!("bs_vol: {}, iv_vol: {}", bs_vol, iv_vol);
38//!
39//! let epsilon = 1e-5;
40//! assert!((bs_vol - iv_vol).abs() < epsilon);
41//! ```
42//!
43
44use serde::{Deserialize, Serialize};
45use thiserror::Error;
46
47use crate::black_scholes;
48
49/// # Implied Volatility Scholes wrapper
50///
51/// Contains the input and output for the implied volatility calculation.
52pub struct ImpliedVol {
53    /// input parameters
54    pub input: Input,
55    /// output result
56    pub output: Result<Output, ImpliedVolError>,
57}
58
59/// # Implied volatility input
60///
61/// This is the input to the implied volatility function reversing the Black Scholes model.  
62///
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct Input {
65    /// call option price observed in the market
66    pub price: f32,
67    /// underlying spot, in currency
68    pub spot: f32,
69    /// option strike, in currency
70    pub strike: f32,
71    /// option maturity, in years
72    pub mat: f32,
73    /// interest rate, annualized, e.g. 0.05=5%
74    pub rate: f32,
75    /// underlying dividend yield, annualized, e.g. 0.02=2%
76    pub div: f32,
77    /// maximum number of iterations - optional
78    pub iter: u32,
79    /// target precision - optional
80    pub prec: f32,
81}
82
83/// # Implied volatility output
84///
85/// This is the output of the implied volatility function reversing the Black Scholes model.
86///
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct Output {
89    /// volatility calculated
90    pub vol: f32,
91    /// iterations run
92    pub iter: u32,
93    /// precision reached
94    pub prec: f32,
95}
96
97/// # Implied Vol Input Error
98///
99/// This covers the various ways implied volatility input params can be invalid.
100#[derive(Error, Debug, Serialize, Deserialize)]
101pub enum ImpliedVolError {
102    /// Negative spot
103    #[error("negative spot: {0} - must be positive")]
104    NegativeSpot(f32),
105    /// Negative strike
106    #[error("negative strike: {0} - must be positive")]
107    NegativeStrike(f32),
108    /// Negative maturity
109    #[error("negative maturity: {0} - must be positive")]
110    NegativeMat(f32),
111    /// Out of bound price
112    #[error("out of bound price: {0} - must be between max(0, S-PV(K)) and spot")]
113    OutOfBoundPrice(f32),
114}
115
116/// # Convergence Method
117///
118/// This enum defines the method used to search for the implied volatility.  
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub enum Method {
121    /// [Newton-Raphson](https://en.wikipedia.org/wiki/Newton%27s_method) method
122    Newton,
123    /// [Halley's](https://en.wikipedia.org/wiki/Halley%27s_method) method
124    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        // newton method
190        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        // halley method
227        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}