implied_vol/builder/
implied_black_volatility.rs

1use crate::{SpecialFn, lets_be_rational};
2use bon::Builder;
3
4/// Builder-backed container that represents the inputs required to compute
5/// the **implied Black volatility** for an undiscounted European option.
6///
7/// Use the generated `ImpliedBlackVolatility::builder()` to construct this
8/// type. The builder performs input validation when you call `.build()`.
9/// Or use `.build_unchecked()` to skip validation.
10///
11/// Fields:
12/// - `forward`: forward price of the underlying (F). Must be finite and `> 0`.
13/// - `strike`: strike price (K). Must be finite and `> 0`.
14/// - `expiry`: time to expiry (T). Must be finite and `>= 0`.
15/// - `is_call`: `true` for a call option, `false` for a put option.
16/// - `option_price`: observed undiscounted option price (P). Must be finite and `>= 0`.
17///
18/// This struct is consumed by `calculate::<SpFn>()` which performs the numerical
19/// inversion `BS(F, K, T, σ) = P` to find the implied volatility `σ`.
20#[derive(Builder)]
21#[builder(const, derive(Clone, Debug),
22finish_fn(name = build_unchecked,
23doc{
24/// Build without performing any validation.
25///
26/// This constructor constructs the `ImpliedBlackVolatility` directly from
27/// the builder's fields and does **not** check for NaNs, infinities, or
28/// sign constraints. Use only when you are certain the inputs are valid
29/// or when you want to avoid the cost of runtime validation.
30})
31)]
32pub struct ImpliedBlackVolatility {
33    forward: f64,
34    strike: f64,
35    expiry: f64,
36    is_call: bool,
37    option_price: f64,
38}
39
40impl<S: implied_black_volatility_builder::IsComplete> ImpliedBlackVolatilityBuilder<S> {
41    /// Validate inputs and build an `ImpliedBlackVolatility`.
42    ///
43    /// Performs the following validation checks:
44    /// - `forward` must be positive and finite.
45    /// - `strike` must be positive and finite.
46    /// - `expiry` must be non-negative (`T >= 0`) but can be positive infinite.
47    /// - `option_price` must be a finite, non-negative number.
48    ///
49    /// Returns `Some(ImpliedBlackVolatility)` when all checks pass, otherwise `None`.
50    ///
51    /// # Rationale
52    /// These checks ensure the constructed object lies within the mathematical domain
53    /// required by the Black–Scholes pricing function. Use `build_unchecked()` to skip validation.
54    pub const fn build(self) -> Option<ImpliedBlackVolatility> {
55        let implied_black_volatility = self.build_unchecked();
56
57        if !(implied_black_volatility.forward > 0.0)
58            || implied_black_volatility.forward.is_infinite()
59        {
60            return None;
61        }
62        if !(implied_black_volatility.strike > 0.0) || implied_black_volatility.strike.is_infinite()
63        {
64            return None;
65        }
66        if !(implied_black_volatility.expiry >= 0.0) {
67            return None;
68        }
69        if !(implied_black_volatility.option_price >= 0.0)
70            || implied_black_volatility.option_price.is_infinite()
71        {
72            return None;
73        }
74        Some(implied_black_volatility)
75    }
76}
77
78impl ImpliedBlackVolatility {
79    /// Compute the implied Black volatility `σ` for the stored inputs.
80    ///
81    /// Returns:
82    /// - `Some(σ)` if an implied volatility consistent with `BS(F, K, T, σ) = P`
83    ///   exists, and the numerical routine converges to a finite value.
84    /// - `None` if the given `option_price` is outside the attainable range for
85    ///   the supplied model parameters.
86    ///
87    /// # Type parameter
88    /// - `SpFn: SpecialFn` — implementation of the `SpecialFn` trait used for
89    ///   internal special-function computations. Use `DefaultSpecialFn` for the
90    ///   crate-provided default behavior, or supply your own implementation
91    ///   to change numerical characteristics.
92    ///
93    /// # Examples
94    ///
95    /// ```rust
96    /// use implied_vol::{DefaultSpecialFn, ImpliedBlackVolatility};
97    ///
98    /// let iv = ImpliedBlackVolatility::builder()
99    ///     .option_price(10.0)
100    ///     .forward(100.0)
101    ///     .strike(100.0)
102    ///     .expiry(1.0)
103    ///     .is_call(true)
104    ///     .build().unwrap();
105    ///
106    /// let sigma = iv.calculate::<DefaultSpecialFn>().unwrap();
107    /// assert!(sigma.is_finite());
108    /// ```
109    #[must_use]
110    #[inline(always)]
111    pub fn calculate<SpFn: SpecialFn>(&self) -> Option<f64> {
112        if self.is_call {
113            lets_be_rational::implied_black_volatility_input_unchecked::<SpFn, true>(
114                self.option_price,
115                self.forward,
116                self.strike,
117                self.expiry,
118            )
119        } else {
120            lets_be_rational::implied_black_volatility_input_unchecked::<SpFn, false>(
121                self.option_price,
122                self.forward,
123                self.strike,
124                self.expiry,
125            )
126        }
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use crate::DefaultSpecialFn;
133    use crate::builder::implied_black_volatility::ImpliedBlackVolatility;
134
135    #[test]
136    const fn normal_const() {
137        const PRICE: f64 = 10.0;
138        const F: f64 = 100.0;
139        const K: f64 = 100.0;
140        const T: f64 = 1.0;
141        const Q: bool = true;
142
143        const IV_BUILDER: Option<ImpliedBlackVolatility> = ImpliedBlackVolatility::builder()
144            .option_price(PRICE)
145            .forward(F)
146            .strike(K)
147            .expiry(T)
148            .is_call(Q)
149            .build();
150        assert!(IV_BUILDER.is_some());
151    }
152
153    #[test]
154    fn strike_anomaly() {
155        for k in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, 0.0] {
156            let price = 100.0;
157            let f = 100.0;
158            let t = 1.0;
159            const Q: bool = true;
160            assert!(
161                ImpliedBlackVolatility::builder()
162                    .option_price(price)
163                    .forward(f)
164                    .strike(k)
165                    .expiry(t)
166                    .is_call(Q)
167                    .build()
168                    .is_none()
169            );
170        }
171    }
172
173    #[test]
174    fn strike_boundary() {
175        let price = 100.0;
176        let f = 100.0;
177        let k = f64::MIN_POSITIVE;
178        let t = 1.0;
179        const Q: bool = true;
180        assert!(
181            ImpliedBlackVolatility::builder()
182                .option_price(price)
183                .forward(f)
184                .strike(k)
185                .expiry(t)
186                .is_call(Q)
187                .build()
188                .is_some()
189        );
190    }
191
192    #[test]
193    fn forward_anomaly() {
194        for f in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, 0.0] {
195            let price = 100.0;
196            let k = 100.0;
197            let t = 1.0;
198            const Q: bool = true;
199            assert!(
200                ImpliedBlackVolatility::builder()
201                    .option_price(price)
202                    .forward(f)
203                    .strike(k)
204                    .expiry(t)
205                    .is_call(Q)
206                    .build()
207                    .is_none()
208            );
209        }
210    }
211
212    #[test]
213    fn price_anomaly() {
214        for price in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
215            let f = 100.0;
216            let t = 1.0;
217            let k = 100.0;
218            const Q: bool = true;
219            assert!(
220                ImpliedBlackVolatility::builder()
221                    .option_price(price)
222                    .forward(f)
223                    .strike(k)
224                    .expiry(t)
225                    .is_call(Q)
226                    .build()
227                    .is_none()
228            );
229        }
230    }
231
232    #[test]
233    fn price_below_intrinsic() {
234        let price = 10.0;
235        let f = 120.0;
236        let t = 1.0;
237        let k = 100.0;
238        const Q: bool = true;
239        let iv_builder = ImpliedBlackVolatility::builder()
240            .option_price(price)
241            .forward(f)
242            .strike(k)
243            .expiry(t)
244            .is_call(Q)
245            .build();
246        assert!(iv_builder.is_some());
247        let iv = iv_builder.unwrap();
248        assert!(iv.calculate::<DefaultSpecialFn>().is_none());
249    }
250
251    #[test]
252    fn time_anomaly() {
253        for t in [f64::NAN, f64::NEG_INFINITY] {
254            let price = 10.0;
255            let f = 100.0;
256            let k = 100.0;
257            const Q: bool = true;
258            assert!(
259                ImpliedBlackVolatility::builder()
260                    .option_price(price)
261                    .forward(f)
262                    .strike(k)
263                    .expiry(t)
264                    .is_call(Q)
265                    .build()
266                    .is_none()
267            );
268        }
269    }
270
271    #[test]
272    fn time_zero() {
273        // the price is below intrinsic
274        let price = 10.0;
275        let f = 120.0;
276        let k = 100.0;
277        let t = 0.0;
278        let q = true;
279
280        let vol = ImpliedBlackVolatility::builder()
281            .option_price(price)
282            .forward(f)
283            .strike(k)
284            .expiry(t)
285            .is_call(q)
286            .build()
287            .unwrap()
288            .calculate::<DefaultSpecialFn>();
289        assert!(vol.is_none());
290
291        let price = 20.0;
292        let f = 120.0;
293        let k = 100.0;
294        let t = 0.0;
295
296        let vol = ImpliedBlackVolatility::builder()
297            .option_price(price)
298            .forward(f)
299            .strike(k)
300            .expiry(t)
301            .is_call(q)
302            .build()
303            .unwrap()
304            .calculate::<DefaultSpecialFn>();
305        assert_eq!(vol.unwrap(), 0.0);
306    }
307
308    #[test]
309    fn time_inf() {
310        let price = 10.0;
311        let f = 100.0;
312        let k = 100.0;
313        let t = f64::INFINITY;
314        const Q: bool = true;
315
316        let vol = ImpliedBlackVolatility::builder()
317            .option_price(price)
318            .forward(f)
319            .strike(k)
320            .expiry(t)
321            .is_call(Q)
322            .build()
323            .unwrap()
324            .calculate::<DefaultSpecialFn>()
325            .unwrap();
326        assert_eq!(vol, 0.0);
327    }
328}