1use core::{convert::Infallible, fmt};
17
18#[derive(Debug, Clone, Copy, PartialEq)]
36pub enum ParamError {
37 EmptyCollection {
39 name: &'static str,
41 },
42 NegativeSlope {
44 b: f64,
46 },
47 CorrelationOutOfRange {
49 rho: f64,
51 },
52 NonPositiveSigma {
54 sigma: f64,
56 },
57 NegativeMinVariance {
60 w_min: f64,
62 },
63 NonPositiveMaturity {
65 t: f64,
67 },
68 NegativeWeight {
70 weight: f64,
72 },
73 NegativeTotalVariance {
75 w: f64,
77 },
78 InvalidPhiParameter {
81 name: &'static str,
83 value: f64,
85 },
86 NonPositiveTheta {
88 theta: f64,
90 },
91 NonFinite {
94 name: &'static str,
96 },
97 NotStrictlyIncreasing {
99 name: &'static str,
101 index: usize,
103 previous: f64,
105 value: f64,
107 },
108 DecreasingAtmVariance {
110 index: usize,
112 previous: f64,
114 value: f64,
116 },
117}
118
119impl fmt::Display for ParamError {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 match self {
122 Self::EmptyCollection { name } => write!(f, "{name} must not be empty"),
123 Self::NegativeSlope { b } => {
124 write!(f, "raw SVI slope b must be non-negative, got {b}")
125 }
126 Self::CorrelationOutOfRange { rho } => {
127 write!(f, "correlation rho must lie in (-1, 1), got {rho}")
128 }
129 Self::NonPositiveSigma { sigma } => {
130 write!(f, "raw SVI curvature sigma must be positive, got {sigma}")
131 }
132 Self::NegativeMinVariance { w_min } => {
133 write!(
134 f,
135 "minimum total variance must be non-negative, got w_min = {w_min}"
136 )
137 }
138 Self::NonPositiveMaturity { t } => {
139 write!(f, "maturity t must be positive, got {t}")
140 }
141 Self::NegativeWeight { weight } => {
142 write!(f, "quote weight must be non-negative, got {weight}")
143 }
144 Self::NegativeTotalVariance { w } => {
145 write!(f, "quoted total variance must be non-negative, got {w}")
146 }
147 Self::InvalidPhiParameter { name, value } => {
148 write!(f, "SSVI phi parameter {name} is out of range: {value}")
149 }
150 Self::NonPositiveTheta { theta } => {
151 write!(f, "SSVI ATM variance theta must be positive, got {theta}")
152 }
153 Self::NonFinite { name } => {
154 write!(f, "input {name} must be a finite number")
155 }
156 Self::NotStrictlyIncreasing {
157 name,
158 index,
159 previous,
160 value,
161 } => write!(
162 f,
163 "{name} must be strictly increasing; index {index} has {value} after {previous}"
164 ),
165 Self::DecreasingAtmVariance {
166 index,
167 previous,
168 value,
169 } => write!(
170 f,
171 "ATM total variance must be non-decreasing; index {index} has {value} after {previous}"
172 ),
173 }
174 }
175}
176
177impl std::error::Error for ParamError {}
178
179impl From<Infallible> for ParamError {
180 fn from(value: Infallible) -> Self {
181 match value {}
182 }
183}
184
185#[derive(Debug, Clone, Copy, PartialEq)]
203pub enum ConvertError {
204 JwHasNoRawPreimage {
206 beta: f64,
208 },
209 NegativeWingSlope {
211 name: &'static str,
213 value: f64,
215 },
216 NonPositiveAtmVariance {
218 w: f64,
220 },
221 DegenerateJw,
224 Param(ParamError),
226}
227
228impl fmt::Display for ConvertError {
229 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230 match self {
231 Self::JwHasNoRawPreimage { beta } => {
232 write!(
233 f,
234 "Jump-Wings tuple has no raw SVI pre-image: |beta| > 1, beta = {beta}"
235 )
236 }
237 Self::NegativeWingSlope { name, value } => {
238 write!(
239 f,
240 "Jump-Wings wing slope {name} must be non-negative, got {value}"
241 )
242 }
243 Self::NonPositiveAtmVariance { w } => {
244 write!(f, "Jump-Wings ATM total variance must be positive, got {w}")
245 }
246 Self::DegenerateJw => {
247 write!(
248 f,
249 "Jump-Wings tuple is degenerate: inverse map is indeterminate"
250 )
251 }
252 Self::Param(e) => write!(f, "converted slice is invalid: {e}"),
253 }
254 }
255}
256
257impl std::error::Error for ConvertError {
258 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
259 match self {
260 Self::Param(e) => Some(e),
261 _ => None,
262 }
263 }
264}
265
266impl From<ParamError> for ConvertError {
267 fn from(e: ParamError) -> Self {
268 Self::Param(e)
269 }
270}
271
272#[derive(Debug, Clone, Copy, PartialEq)]
290pub enum CalibrationError {
291 EmptyQuotes,
293 DidNotConverge {
295 iterations: usize,
297 residual: f64,
299 },
300 AllWeightsZero,
302 InsufficientEffectiveQuotes {
304 usable: usize,
306 distinct: usize,
308 need: usize,
310 },
311 InsufficientThetaLevels {
314 got: usize,
316 need: usize,
318 },
319 InvalidConfig {
321 field: &'static str,
323 value: f64,
325 },
326 Infeasible {
328 condition: &'static str,
330 margin: f64,
332 },
333 Param(ParamError),
335}
336
337impl fmt::Display for CalibrationError {
338 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
339 match self {
340 Self::EmptyQuotes => write!(f, "quote set is empty"),
341 Self::DidNotConverge {
342 iterations,
343 residual,
344 } => {
345 write!(
346 f,
347 "calibration did not converge after {iterations} iterations, residual = {residual}"
348 )
349 }
350 Self::AllWeightsZero => write!(f, "all fitting weights are zero"),
351 Self::InsufficientEffectiveQuotes {
352 usable,
353 distinct,
354 need,
355 } => write!(
356 f,
357 "insufficient effective quotes: {usable} positive-weight, {distinct} distinct, need {need}"
358 ),
359 Self::InsufficientThetaLevels { got, need } => write!(
360 f,
361 "insufficient distinct ATM-variance levels: got {got}, need at least {need}"
362 ),
363 Self::InvalidConfig { field, value } => {
364 write!(f, "invalid calibration control {field} = {value}")
365 }
366 Self::Infeasible { condition, margin } => {
367 write!(
368 f,
369 "constrained calibration failed {condition}, margin = {margin}"
370 )
371 }
372 Self::Param(e) => write!(f, "calibrated slice is invalid: {e}"),
373 }
374 }
375}
376
377impl std::error::Error for CalibrationError {
378 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
379 match self {
380 Self::Param(e) => Some(e),
381 _ => None,
382 }
383 }
384}
385
386impl From<ParamError> for CalibrationError {
387 fn from(e: ParamError) -> Self {
388 Self::Param(e)
389 }
390}
391
392#[cfg(test)]
393#[allow(clippy::expect_used)] mod tests {
395 use super::*;
396
397 #[test]
398 fn param_error_display_negative_slope() {
399 let err = ParamError::NegativeSlope { b: -0.1 };
400 assert_eq!(
401 format!("{err}"),
402 "raw SVI slope b must be non-negative, got -0.1"
403 );
404 }
405
406 #[test]
407 fn param_error_display_correlation() {
408 let err = ParamError::CorrelationOutOfRange { rho: 1.5 };
409 assert!(format!("{err}").contains("1.5"));
410 }
411
412 #[test]
413 fn param_error_display_non_positive_sigma() {
414 let err = ParamError::NonPositiveSigma { sigma: 0.0 };
415 assert!(format!("{err}").contains("sigma"));
416 }
417
418 #[test]
419 fn param_error_display_negative_min_variance() {
420 let err = ParamError::NegativeMinVariance { w_min: -0.01 };
421 assert!(format!("{err}").contains("w_min"));
422 }
423
424 #[test]
425 fn param_error_display_remaining_variants() {
426 assert!(format!("{}", ParamError::NonPositiveMaturity { t: 0.0 }).contains("maturity"));
427 assert!(format!("{}", ParamError::NegativeWeight { weight: -1.0 }).contains("weight"));
428 assert!(
429 format!("{}", ParamError::NegativeTotalVariance { w: -0.1 }).contains("total variance")
430 );
431 assert!(
432 format!(
433 "{}",
434 ParamError::InvalidPhiParameter {
435 name: "eta",
436 value: -1.0
437 }
438 )
439 .contains("eta")
440 );
441 assert!(format!("{}", ParamError::NonPositiveTheta { theta: 0.0 }).contains("theta"));
442 assert!(format!("{}", ParamError::NonFinite { name: "k" }).contains("finite"));
443 }
444
445 #[test]
446 fn param_error_is_error_trait() {
447 let err: &dyn std::error::Error = &ParamError::NegativeSlope { b: -1.0 };
448 assert!(err.source().is_none());
449 }
450
451 #[test]
452 fn param_error_copy_eq() {
453 let err = ParamError::NonFinite { name: "x" };
454 let copy = err;
455 assert_eq!(err, copy);
456 }
457
458 #[test]
459 fn convert_error_display() {
460 let err = ConvertError::JwHasNoRawPreimage { beta: 1.4 };
461 assert!(format!("{err}").contains("1.4"));
462 let err = ConvertError::NegativeWingSlope {
463 name: "p_t",
464 value: -1.0,
465 };
466 assert!(format!("{err}").contains("p_t"));
467 assert!(format!("{}", ConvertError::DegenerateJw).contains("degenerate"));
468 assert!(
469 format!("{}", ConvertError::NonPositiveAtmVariance { w: -0.1 }).contains("positive")
470 );
471 }
472
473 #[test]
474 fn convert_error_from_param_and_source() {
475 let pe = ParamError::NegativeSlope { b: -1.0 };
476 let ce: ConvertError = pe.into();
477 assert!(matches!(ce, ConvertError::Param(_)));
478 let dyn_err: &dyn std::error::Error = &ce;
479 assert!(dyn_err.source().is_some());
480 }
481
482 #[test]
483 fn calibration_error_display() {
484 let err = CalibrationError::InsufficientEffectiveQuotes {
485 usable: 2,
486 distinct: 2,
487 need: 5,
488 };
489 let msg = format!("{err}");
490 assert!(msg.contains('2') && msg.contains('5'));
491 assert!(format!("{}", CalibrationError::EmptyQuotes).contains("empty"));
492 assert!(
493 format!(
494 "{}",
495 CalibrationError::DidNotConverge {
496 iterations: 100,
497 residual: 1e-3
498 }
499 )
500 .contains("converge")
501 );
502 assert!(format!("{}", CalibrationError::AllWeightsZero).contains("weights"));
503 }
504
505 #[test]
506 fn calibration_error_from_param_and_source() {
507 let pe = ParamError::NonPositiveSigma { sigma: 0.0 };
508 let ce: CalibrationError = pe.into();
509 assert!(matches!(ce, CalibrationError::Param(_)));
510 let dyn_err: &dyn std::error::Error = &ce;
511 assert!(dyn_err.source().is_some());
512 }
513
514 #[test]
515 fn errors_debug() {
516 assert!(format!("{:?}", ParamError::NonFinite { name: "k" }).contains("NonFinite"));
517 assert!(format!("{:?}", ConvertError::DegenerateJw).contains("Degenerate"));
518 assert!(format!("{:?}", CalibrationError::EmptyQuotes).contains("Empty"));
519 }
520}