regit_blackscholes/
errors.rs1use core::fmt;
11
12#[derive(Debug, Clone, Copy, PartialEq)]
29pub enum PricingError {
30 NegativeSpot,
32 NegativeStrike,
34 NegativeTime,
36 NegativeVolatility,
38 IntrinsicOnly {
42 intrinsic: f64,
44 },
45}
46
47impl fmt::Display for PricingError {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 match self {
50 Self::NegativeSpot => write!(f, "spot price must be non-negative"),
51 Self::NegativeStrike => write!(f, "strike price must be non-negative"),
52 Self::NegativeTime => write!(f, "time to expiry must be non-negative"),
53 Self::NegativeVolatility => write!(f, "volatility must be non-negative"),
54 Self::IntrinsicOnly { intrinsic } => {
55 write!(f, "option at expiry: intrinsic value = {intrinsic}")
56 }
57 }
58 }
59}
60
61impl std::error::Error for PricingError {}
62
63#[derive(Debug, Clone, Copy, PartialEq)]
80pub enum IvError {
81 NoSolution,
83 BelowIntrinsic {
85 intrinsic: f64,
87 },
88 MaxIterationsReached {
90 last_vol: f64,
92 residual: f64,
94 },
95 NearZeroVega,
98 BoundsExceeded {
100 vol: f64,
102 },
103}
104
105impl fmt::Display for IvError {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 match self {
108 Self::NoSolution => write!(f, "no implied volatility solution exists"),
109 Self::BelowIntrinsic { intrinsic } => {
110 write!(f, "market price is below intrinsic value ({intrinsic})")
111 }
112 Self::MaxIterationsReached { last_vol, residual } => {
113 write!(
114 f,
115 "IV solver did not converge: last_vol = {last_vol}, residual = {residual}"
116 )
117 }
118 Self::NearZeroVega => write!(f, "vega is near zero — solver cannot make progress"),
119 Self::BoundsExceeded { vol } => {
120 write!(f, "implied volatility {vol} exceeds search bounds")
121 }
122 }
123 }
124}
125
126impl std::error::Error for IvError {}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn test_pricing_error_display_negative_spot() {
134 let err = PricingError::NegativeSpot;
135 assert_eq!(format!("{err}"), "spot price must be non-negative");
136 }
137
138 #[test]
139 fn test_pricing_error_display_negative_strike() {
140 let err = PricingError::NegativeStrike;
141 assert_eq!(format!("{err}"), "strike price must be non-negative");
142 }
143
144 #[test]
145 fn test_pricing_error_display_negative_time() {
146 let err = PricingError::NegativeTime;
147 assert_eq!(format!("{err}"), "time to expiry must be non-negative");
148 }
149
150 #[test]
151 fn test_pricing_error_display_negative_volatility() {
152 let err = PricingError::NegativeVolatility;
153 assert_eq!(format!("{err}"), "volatility must be non-negative");
154 }
155
156 #[test]
157 fn test_pricing_error_display_intrinsic_only() {
158 let err = PricingError::IntrinsicOnly {
159 intrinsic: 5.25_f64,
160 };
161 assert_eq!(format!("{err}"), "option at expiry: intrinsic value = 5.25");
162 }
163
164 #[test]
165 fn test_pricing_error_is_error_trait() {
166 let err: &dyn std::error::Error = &PricingError::NegativeSpot;
167 assert!(err.source().is_none());
168 }
169
170 #[test]
171 fn test_pricing_error_clone_copy() {
172 let err = PricingError::NegativeSpot;
173 let err2 = err;
174 assert_eq!(err, err2);
175 }
176
177 #[test]
178 fn test_pricing_error_debug() {
179 let err = PricingError::NegativeSpot;
180 let debug = format!("{err:?}");
181 assert!(debug.contains("NegativeSpot"));
182 }
183
184 #[test]
185 fn test_iv_error_display_no_solution() {
186 let err = IvError::NoSolution;
187 assert_eq!(format!("{err}"), "no implied volatility solution exists");
188 }
189
190 #[test]
191 fn test_iv_error_display_below_intrinsic() {
192 let err = IvError::BelowIntrinsic {
193 intrinsic: 10.0_f64,
194 };
195 assert_eq!(
196 format!("{err}"),
197 "market price is below intrinsic value (10)"
198 );
199 }
200
201 #[test]
202 fn test_iv_error_display_max_iterations() {
203 let err = IvError::MaxIterationsReached {
204 last_vol: 0.25_f64,
205 residual: 0.001_f64,
206 };
207 let msg = format!("{err}");
208 assert!(msg.contains("last_vol = 0.25"));
209 assert!(msg.contains("residual = 0.001"));
210 }
211
212 #[test]
213 fn test_iv_error_display_near_zero_vega() {
214 let err = IvError::NearZeroVega;
215 let msg = format!("{err}");
216 assert!(msg.contains("vega is near zero"));
217 }
218
219 #[test]
220 fn test_iv_error_display_bounds_exceeded() {
221 let err = IvError::BoundsExceeded { vol: 150.0_f64 };
222 let msg = format!("{err}");
223 assert!(msg.contains("150"));
224 assert!(msg.contains("exceeds search bounds"));
225 }
226
227 #[test]
228 fn test_iv_error_is_error_trait() {
229 let err: &dyn std::error::Error = &IvError::NoSolution;
230 assert!(err.source().is_none());
231 }
232
233 #[test]
234 fn test_iv_error_clone_copy() {
235 let err = IvError::NearZeroVega;
236 let err2 = err;
237 assert_eq!(err, err2);
238 }
239
240 #[test]
241 fn test_iv_error_debug() {
242 let err = IvError::BoundsExceeded { vol: 200.0_f64 };
243 let debug = format!("{err:?}");
244 assert!(debug.contains("BoundsExceeded"));
245 }
246
247 #[test]
248 fn test_pricing_error_eq() {
249 assert_eq!(PricingError::NegativeSpot, PricingError::NegativeSpot);
250 assert_ne!(PricingError::NegativeSpot, PricingError::NegativeStrike);
251 }
252
253 #[test]
254 fn test_iv_error_eq() {
255 assert_eq!(IvError::NoSolution, IvError::NoSolution);
256 assert_ne!(IvError::NoSolution, IvError::NearZeroVega);
257 }
258
259 #[test]
260 fn test_pricing_error_intrinsic_only_zero() {
261 let err = PricingError::IntrinsicOnly { intrinsic: 0.0_f64 };
262 if let PricingError::IntrinsicOnly { intrinsic } = err {
263 assert!((intrinsic - 0.0_f64).abs() < 1e-15_f64);
264 }
265 }
266
267 #[test]
268 fn test_iv_error_max_iterations_fields() {
269 let err = IvError::MaxIterationsReached {
270 last_vol: 0.3_f64,
271 residual: 1e-8_f64,
272 };
273 if let IvError::MaxIterationsReached { last_vol, residual } = err {
274 assert!((last_vol - 0.3_f64).abs() < 1e-15_f64);
275 assert!((residual - 1e-8_f64).abs() < 1e-20_f64);
276 }
277 }
278}