1use core::fmt;
17
18#[derive(Debug, Clone, Copy, PartialEq)]
36pub enum ParamError {
37 NegativeSlope {
39 b: f64,
41 },
42 CorrelationOutOfRange {
44 rho: f64,
46 },
47 NonPositiveSigma {
49 sigma: f64,
51 },
52 NegativeMinVariance {
55 w_min: f64,
57 },
58 NonPositiveMaturity {
60 t: f64,
62 },
63 NegativeWeight {
65 weight: f64,
67 },
68 NegativeTotalVariance {
70 w: f64,
72 },
73 InvalidPhiParameter {
76 name: &'static str,
78 value: f64,
80 },
81 NonPositiveTheta {
83 theta: f64,
85 },
86 NonFinite {
89 name: &'static str,
91 },
92}
93
94impl fmt::Display for ParamError {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 match self {
97 Self::NegativeSlope { b } => {
98 write!(f, "raw SVI slope b must be non-negative, got {b}")
99 }
100 Self::CorrelationOutOfRange { rho } => {
101 write!(f, "correlation rho must lie in (-1, 1), got {rho}")
102 }
103 Self::NonPositiveSigma { sigma } => {
104 write!(f, "raw SVI curvature sigma must be positive, got {sigma}")
105 }
106 Self::NegativeMinVariance { w_min } => {
107 write!(
108 f,
109 "minimum total variance must be non-negative, got w_min = {w_min}"
110 )
111 }
112 Self::NonPositiveMaturity { t } => {
113 write!(f, "maturity t must be positive, got {t}")
114 }
115 Self::NegativeWeight { weight } => {
116 write!(f, "quote weight must be non-negative, got {weight}")
117 }
118 Self::NegativeTotalVariance { w } => {
119 write!(f, "quoted total variance must be non-negative, got {w}")
120 }
121 Self::InvalidPhiParameter { name, value } => {
122 write!(f, "SSVI phi parameter {name} is out of range: {value}")
123 }
124 Self::NonPositiveTheta { theta } => {
125 write!(f, "SSVI ATM variance theta must be positive, got {theta}")
126 }
127 Self::NonFinite { name } => {
128 write!(f, "input {name} must be a finite number")
129 }
130 }
131 }
132}
133
134impl std::error::Error for ParamError {}
135
136#[derive(Debug, Clone, Copy, PartialEq)]
154pub enum ConvertError {
155 JwHasNoRawPreimage {
157 beta: f64,
159 },
160 NegativeWingSlope {
162 name: &'static str,
164 value: f64,
166 },
167 NonPositiveAtmVariance {
169 w: f64,
171 },
172 DegenerateJw,
175 Param(ParamError),
177}
178
179impl fmt::Display for ConvertError {
180 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181 match self {
182 Self::JwHasNoRawPreimage { beta } => {
183 write!(
184 f,
185 "Jump-Wings tuple has no raw SVI pre-image: |beta| > 1, beta = {beta}"
186 )
187 }
188 Self::NegativeWingSlope { name, value } => {
189 write!(
190 f,
191 "Jump-Wings wing slope {name} must be non-negative, got {value}"
192 )
193 }
194 Self::NonPositiveAtmVariance { w } => {
195 write!(f, "Jump-Wings ATM total variance must be positive, got {w}")
196 }
197 Self::DegenerateJw => {
198 write!(
199 f,
200 "Jump-Wings tuple is degenerate: inverse map is indeterminate"
201 )
202 }
203 Self::Param(e) => write!(f, "converted slice is invalid: {e}"),
204 }
205 }
206}
207
208impl std::error::Error for ConvertError {
209 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
210 match self {
211 Self::Param(e) => Some(e),
212 _ => None,
213 }
214 }
215}
216
217impl From<ParamError> for ConvertError {
218 fn from(e: ParamError) -> Self {
219 Self::Param(e)
220 }
221}
222
223#[derive(Debug, Clone, Copy, PartialEq)]
241pub enum CalibrationError {
242 TooFewQuotes {
244 got: usize,
246 need: usize,
248 },
249 EmptyQuotes,
251 DidNotConverge {
253 iterations: usize,
255 residual: f64,
257 },
258 AllWeightsZero,
260 Param(ParamError),
262}
263
264impl fmt::Display for CalibrationError {
265 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266 match self {
267 Self::TooFewQuotes { got, need } => {
268 write!(
269 f,
270 "too few quotes for calibration: got {got}, need at least {need}"
271 )
272 }
273 Self::EmptyQuotes => write!(f, "quote set is empty"),
274 Self::DidNotConverge {
275 iterations,
276 residual,
277 } => {
278 write!(
279 f,
280 "calibration did not converge after {iterations} iterations, residual = {residual}"
281 )
282 }
283 Self::AllWeightsZero => write!(f, "all fitting weights are zero"),
284 Self::Param(e) => write!(f, "calibrated slice is invalid: {e}"),
285 }
286 }
287}
288
289impl std::error::Error for CalibrationError {
290 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
291 match self {
292 Self::Param(e) => Some(e),
293 _ => None,
294 }
295 }
296}
297
298impl From<ParamError> for CalibrationError {
299 fn from(e: ParamError) -> Self {
300 Self::Param(e)
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn param_error_display_negative_slope() {
310 let err = ParamError::NegativeSlope { b: -0.1 };
311 assert_eq!(
312 format!("{err}"),
313 "raw SVI slope b must be non-negative, got -0.1"
314 );
315 }
316
317 #[test]
318 fn param_error_display_correlation() {
319 let err = ParamError::CorrelationOutOfRange { rho: 1.5 };
320 assert!(format!("{err}").contains("1.5"));
321 }
322
323 #[test]
324 fn param_error_display_non_positive_sigma() {
325 let err = ParamError::NonPositiveSigma { sigma: 0.0 };
326 assert!(format!("{err}").contains("sigma"));
327 }
328
329 #[test]
330 fn param_error_display_negative_min_variance() {
331 let err = ParamError::NegativeMinVariance { w_min: -0.01 };
332 assert!(format!("{err}").contains("w_min"));
333 }
334
335 #[test]
336 fn param_error_display_remaining_variants() {
337 assert!(format!("{}", ParamError::NonPositiveMaturity { t: 0.0 }).contains("maturity"));
338 assert!(format!("{}", ParamError::NegativeWeight { weight: -1.0 }).contains("weight"));
339 assert!(
340 format!("{}", ParamError::NegativeTotalVariance { w: -0.1 }).contains("total variance")
341 );
342 assert!(
343 format!(
344 "{}",
345 ParamError::InvalidPhiParameter {
346 name: "eta",
347 value: -1.0
348 }
349 )
350 .contains("eta")
351 );
352 assert!(format!("{}", ParamError::NonPositiveTheta { theta: 0.0 }).contains("theta"));
353 assert!(format!("{}", ParamError::NonFinite { name: "k" }).contains("finite"));
354 }
355
356 #[test]
357 fn param_error_is_error_trait() {
358 let err: &dyn std::error::Error = &ParamError::NegativeSlope { b: -1.0 };
359 assert!(err.source().is_none());
360 }
361
362 #[test]
363 fn param_error_copy_eq() {
364 let err = ParamError::NonFinite { name: "x" };
365 let copy = err;
366 assert_eq!(err, copy);
367 }
368
369 #[test]
370 fn convert_error_display() {
371 let err = ConvertError::JwHasNoRawPreimage { beta: 1.4 };
372 assert!(format!("{err}").contains("1.4"));
373 let err = ConvertError::NegativeWingSlope {
374 name: "p_t",
375 value: -1.0,
376 };
377 assert!(format!("{err}").contains("p_t"));
378 assert!(format!("{}", ConvertError::DegenerateJw).contains("degenerate"));
379 assert!(
380 format!("{}", ConvertError::NonPositiveAtmVariance { w: -0.1 }).contains("positive")
381 );
382 }
383
384 #[test]
385 fn convert_error_from_param_and_source() {
386 let pe = ParamError::NegativeSlope { b: -1.0 };
387 let ce: ConvertError = pe.into();
388 assert!(matches!(ce, ConvertError::Param(_)));
389 let dyn_err: &dyn std::error::Error = &ce;
390 assert!(dyn_err.source().is_some());
391 }
392
393 #[test]
394 fn calibration_error_display() {
395 let err = CalibrationError::TooFewQuotes { got: 2, need: 5 };
396 let msg = format!("{err}");
397 assert!(msg.contains('2') && msg.contains('5'));
398 assert!(format!("{}", CalibrationError::EmptyQuotes).contains("empty"));
399 assert!(
400 format!(
401 "{}",
402 CalibrationError::DidNotConverge {
403 iterations: 100,
404 residual: 1e-3
405 }
406 )
407 .contains("converge")
408 );
409 assert!(format!("{}", CalibrationError::AllWeightsZero).contains("weights"));
410 }
411
412 #[test]
413 fn calibration_error_from_param_and_source() {
414 let pe = ParamError::NonPositiveSigma { sigma: 0.0 };
415 let ce: CalibrationError = pe.into();
416 assert!(matches!(ce, CalibrationError::Param(_)));
417 let dyn_err: &dyn std::error::Error = &ce;
418 assert!(dyn_err.source().is_some());
419 }
420
421 #[test]
422 fn errors_debug() {
423 assert!(format!("{:?}", ParamError::NonFinite { name: "k" }).contains("NonFinite"));
424 assert!(format!("{:?}", ConvertError::DegenerateJw).contains("Degenerate"));
425 assert!(format!("{:?}", CalibrationError::EmptyQuotes).contains("Empty"));
426 }
427}