optionstratlib_pricing/error/volatility.rs
1/******************************************************************************
2 Author: Joaquín Béjar García
3 Email: jb@taunais.com
4 Date: 19/1/25
5******************************************************************************/
6
7//! Target crate (ADR-0001 D6, roadmap M1-14): **pricing**. Owns `VolatilityError`.
8
9use optionstratlib_core::error::OptionsError;
10use optionstratlib_core::model::Positive;
11use thiserror::Error;
12
13/// Represents errors that can occur during volatility-related calculations.
14///
15/// This enum encapsulates various error conditions that may arise during implied volatility
16/// calculations, volatility surface generation, or other volatility-related operations.
17/// It provides detailed context about what went wrong, including invalid inputs, numerical
18/// issues, and convergence failures.
19///
20/// `VolatilityError` is particularly useful for diagnosing problems in option pricing
21/// models that rely on volatility parameters, such as Black-Scholes or binomial models.
22#[derive(Error, Debug)]
23pub enum VolatilityError {
24 /// Error indicating that a price value is invalid for volatility calculations.
25 ///
26 /// This variant is used when a price input doesn't meet the requirements
27 /// for volatility calculation, such as being negative or outside valid bounds.
28 #[error("Invalid price {price}: {reason}")]
29 InvalidPrice {
30 /// The invalid price value that caused the error.
31 price: Positive,
32 /// A description explaining why the price is invalid.
33 reason: String,
34 },
35
36 /// Error indicating that a time value is invalid for volatility calculations.
37 ///
38 /// This occurs when time parameters (such as time to expiration) are invalid
39 /// for volatility calculations, for example being negative or too large.
40 #[error("Invalid time {time}: {reason}")]
41 InvalidTime {
42 /// The invalid time value that caused the error.
43 time: Positive,
44 /// A description explaining why the time value is invalid.
45 reason: String,
46 },
47
48 /// Error indicating that the vega value is zero.
49 ///
50 /// This occurs when attempting to calculate implied volatility using the
51 /// Newton-Raphson method and vega is zero, making it impossible to converge.
52 #[error("Vega is zero, cannot calculate implied volatility")]
53 ZeroVega,
54
55 /// Error related to option calculations.
56 #[error(transparent)]
57 Options(#[from] OptionsError),
58
59 /// Error related to vega calculations or usage.
60 ///
61 /// This represents more general issues with vega calculations beyond just
62 /// zero values.
63 #[error("Error calculating vega: {reason}")]
64 VegaError {
65 /// A description of what went wrong with the vega calculation.
66 reason: String,
67 },
68
69 /// Error related to option calculations or parameters.
70 ///
71 /// This represents issues with the underlying option model or parameters
72 /// that prevent proper volatility calculation.
73 #[error("Option error: {reason}")]
74 OptionError {
75 /// A description of what went wrong with the option calculation.
76 reason: String,
77 },
78
79 /// Error indicating that an iterative volatility calculation failed to converge.
80 ///
81 /// This typically occurs in numerical methods like Newton-Raphson or bisection
82 /// when trying to solve for implied volatility.
83 #[error("No convergence after {iterations} iterations. Last volatility: {last_volatility}")]
84 NoConvergence {
85 /// The number of iterations that were performed before giving up.
86 iterations: u32,
87 /// The last volatility value that was calculated before giving up.
88 last_volatility: Positive,
89 },
90
91 /// No implied-volatility candidate produced a price match after the grid search.
92 #[error("implied volatility not found within search grid")]
93 IvNotFound,
94
95 /// None of the generated volatility samples produced a valid price.
96 #[error("no valid volatility sample produced a finite price")]
97 NoValidVolatility,
98
99 /// A numerical precision or representation failure inside a Heston-style
100 /// volatility simulation kernel (e.g. `sqrt` overflow, `f64`/`Decimal` bridge).
101 #[error("volatility simulation numerical failure: {reason}")]
102 NumericalFailure {
103 /// Human-readable description of which numerical step failed.
104 reason: String,
105 },
106
107 /// The ATM implied volatility is unavailable; the boxed inner source
108 /// describes the failing lookup.
109 #[error("ATM implied volatility is not available: {source}")]
110 AtmIvUnavailable {
111 /// Underlying error that explains why the ATM IV could not be retrieved.
112 #[source]
113 source: Box<VolatilityError>,
114 },
115
116 /// Positive value errors
117 #[error(transparent)]
118 PositiveError(#[from] optionstratlib_core::model::PositiveError),
119
120 /// Decimal arithmetic failures propagated from monetary-flow
121 /// computations in volatility kernels (overflow, division by
122 /// zero, conversion).
123 ///
124 /// Produced when a checked Decimal helper in `model::decimal`
125 /// surfaces a [`optionstratlib_core::error::DecimalError`] from inside a
126 /// volatility routine (for example `constant_volatility`,
127 /// `ewma_volatility`, or `garch_volatility`). Propagated
128 /// transparently via the `#[from]` cascade so callers keep
129 /// matching `VolatilityError`.
130 #[error(transparent)]
131 DecimalError(#[from] optionstratlib_core::error::DecimalError),
132
133 /// A volatility kernel produced a non-finite `f64` value (`NaN` /
134 /// `±∞`) at an `f64` → `Decimal` boundary.
135 ///
136 /// Emitted by the Newton-Raphson implied-volatility solver, the
137 /// bisection fallback, Heston simulation discretisations, and
138 /// any other volatility routine that bridges `f64`
139 /// `sqrt`/`ln`/`exp` results back into `Decimal`. `context` is a
140 /// static call-site tag following the same convention as
141 /// [`optionstratlib_core::error::DecimalError::Overflow`].
142 #[error("volatility non-finite {context}: {value}")]
143 NonFinite {
144 /// Static tag identifying the kernel and step that produced
145 /// the non-finite value.
146 context: &'static str,
147 /// The offending `f64` value (`NaN`, `+∞`, or `-∞`).
148 value: f64,
149 },
150}
151
152impl VolatilityError {
153 /// Creates a [`VolatilityError::NonFinite`] from a static call-site
154 /// tag and the offending `f64` value.
155 #[cold]
156 #[inline(never)]
157 #[must_use]
158 pub fn non_finite(context: &'static str, value: f64) -> Self {
159 VolatilityError::NonFinite { context, value }
160 }
161}
162
163#[cfg(test)]
164mod tests_volatility_errors {
165 use super::*;
166 use crate::error::GreeksError;
167 use crate::error::greeks::InputErrorKind;
168 use optionstratlib_core::error::OptionsError;
169 use optionstratlib_core::pos_or_panic;
170
171 #[test]
172 fn test_invalid_price_error() {
173 let error = VolatilityError::InvalidPrice {
174 price: Positive::ZERO,
175 reason: "Price cannot be zero".to_string(),
176 };
177
178 assert_eq!(error.to_string(), "Invalid price 0: Price cannot be zero");
179 }
180
181 #[test]
182 fn test_invalid_time_error() {
183 let error = VolatilityError::InvalidTime {
184 time: Positive::ZERO,
185 reason: "Time cannot be zero".to_string(),
186 };
187
188 assert_eq!(error.to_string(), "Invalid time 0: Time cannot be zero");
189 }
190
191 #[test]
192 fn test_zero_vega_error() {
193 let error = VolatilityError::ZeroVega;
194
195 assert_eq!(
196 error.to_string(),
197 "Vega is zero, cannot calculate implied volatility"
198 );
199 }
200
201 #[test]
202 fn test_vega_error() {
203 let error = VolatilityError::VegaError {
204 reason: "Failed to calculate vega".to_string(),
205 };
206
207 assert_eq!(
208 error.to_string(),
209 "Error calculating vega: Failed to calculate vega"
210 );
211 }
212
213 #[test]
214 fn test_option_error() {
215 let error = VolatilityError::OptionError {
216 reason: "Invalid option parameters".to_string(),
217 };
218
219 assert_eq!(error.to_string(), "Option error: Invalid option parameters");
220 }
221
222 #[test]
223 fn test_no_convergence_error() {
224 let error = VolatilityError::NoConvergence {
225 iterations: 100,
226 last_volatility: pos_or_panic!(0.5),
227 };
228
229 assert_eq!(
230 error.to_string(),
231 "No convergence after 100 iterations. Last volatility: 0.5"
232 );
233 }
234
235 #[test]
236 fn test_a_greeks_failure_is_reported_as_a_numerical_failure() {
237 // Volatility and Greeks are both pricing capabilities, but the IV
238 // solver reports its own failure rather than re-wrapping the Greek
239 // one (#511); the reason still reaches the caller.
240 let greeks_error = GreeksError::InputError(InputErrorKind::InvalidVolatility {
241 value: 0.0,
242 reason: "Volatility cannot be zero".to_string(),
243 });
244
245 let implied_vol_error = VolatilityError::NumericalFailure {
246 reason: format!("vega: {greeks_error}"),
247 };
248
249 match implied_vol_error {
250 VolatilityError::NumericalFailure { reason } => {
251 assert!(reason.contains("Volatility cannot be zero"), "{reason}");
252 }
253 other => panic!("expected NumericalFailure, got {other:?}"),
254 }
255 }
256
257 #[test]
258 fn test_from_options_error() {
259 let options_error = OptionsError::validation_error("strike", "Invalid option parameters");
260
261 let implied_vol_error: VolatilityError = options_error.into();
262
263 match implied_vol_error {
264 VolatilityError::Options(_) => {
265 // Conversion successful
266 }
267 _ => panic!("Wrong error variant"),
268 }
269 }
270
271 #[test]
272 fn test_iv_not_found() {
273 let error = VolatilityError::IvNotFound;
274 assert_eq!(
275 error.to_string(),
276 "implied volatility not found within search grid"
277 );
278 }
279
280 #[test]
281 fn test_no_valid_volatility() {
282 let error = VolatilityError::NoValidVolatility;
283 assert_eq!(
284 error.to_string(),
285 "no valid volatility sample produced a finite price"
286 );
287 }
288
289 #[test]
290 fn test_atm_iv_unavailable() {
291 let error = VolatilityError::AtmIvUnavailable {
292 source: Box::new(VolatilityError::IvNotFound),
293 };
294 assert!(
295 error
296 .to_string()
297 .contains("ATM implied volatility is not available")
298 );
299 }
300
301 #[test]
302 fn test_error_is_send() {
303 fn assert_send<T: Send>() {}
304 assert_send::<VolatilityError>();
305 }
306
307 #[test]
308 fn test_error_is_sync() {
309 fn assert_sync<T: Sync>() {}
310 assert_sync::<VolatilityError>();
311 }
312}