1use core::fmt;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
51pub enum TypeError {
52 InvalidDate {
56 year: i32,
58 month: u32,
60 day: u32,
62 },
63 NonPositiveRange,
67 NonFinite {
70 name: &'static str,
72 },
73 InvalidTenor {
77 reason: &'static str,
79 },
80}
81
82impl fmt::Display for TypeError {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 match self {
85 Self::InvalidDate { year, month, day } => {
86 write!(f, "invalid calendar date: {year:04}-{month:02}-{day:02}")
87 }
88 Self::NonPositiveRange => write!(f, "day-count range must be strictly positive"),
89 Self::NonFinite { name } => write!(f, "input {name} must be a finite number"),
90 Self::InvalidTenor { reason } => write!(f, "invalid tenor: {reason}"),
91 }
92 }
93}
94
95impl std::error::Error for TypeError {}
96
97#[derive(Debug, Clone, Copy, PartialEq)]
113pub enum CurveError {
114 TooFewNodes {
116 found: usize,
118 },
119 NodesNotIncreasing {
121 at_index: usize,
123 },
124 NonPositiveDiscount {
126 at_index: usize,
128 value: f64,
130 },
131 AnchorNotUnit,
133 InvalidTime {
135 t: f64,
137 },
138 Type(TypeError),
140 DuplicateNode {
143 t: f64,
145 },
146}
147
148impl fmt::Display for CurveError {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 match self {
151 Self::TooFewNodes { found } => {
152 write!(f, "curve needs at least two nodes, found {found}")
153 }
154 Self::NodesNotIncreasing { at_index } => {
155 write!(f, "node times not strictly increasing at index {at_index}")
156 }
157 Self::NonPositiveDiscount { at_index, value } => {
158 write!(
159 f,
160 "discount factor at node {at_index} must be positive, got {value}"
161 )
162 }
163 Self::AnchorNotUnit => write!(f, "anchor node must be (t=0, D=1)"),
164 Self::InvalidTime { t } => write!(f, "invalid time t = {t}"),
165 Self::Type(e) => write!(f, "type error in curve query: {e}"),
166 Self::DuplicateNode { t } => write!(f, "duplicate node at t = {t}"),
167 }
168 }
169}
170
171impl std::error::Error for CurveError {
172 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
173 match self {
174 Self::Type(e) => Some(e),
175 _ => None,
176 }
177 }
178}
179
180impl From<TypeError> for CurveError {
181 fn from(e: TypeError) -> Self {
182 Self::Type(e)
183 }
184}
185
186#[derive(Debug, Clone, Copy, PartialEq)]
201pub enum BootstrapError {
202 InstrumentsNotOrdered {
205 at_index: usize,
207 },
208 NonIncreasingAnchor {
210 at_index: usize,
212 },
213 LegDidNotConverge {
216 at_index: usize,
218 residual: f64,
220 },
221 NoBracket {
223 at_index: usize,
225 },
226 InvalidInstrument {
229 at_index: usize,
231 reason: &'static str,
233 },
234 Curve(CurveError),
236 Type(TypeError),
238}
239
240impl fmt::Display for BootstrapError {
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 match self {
243 Self::InstrumentsNotOrdered { at_index } => {
244 write!(f, "instruments not ordered at index {at_index}")
245 }
246 Self::NonIncreasingAnchor { at_index } => {
247 write!(
248 f,
249 "instrument anchor not strictly increasing at index {at_index}"
250 )
251 }
252 Self::LegDidNotConverge { at_index, residual } => {
253 write!(
254 f,
255 "bootstrap leg {at_index} did not converge: residual {residual:e}"
256 )
257 }
258 Self::NoBracket { at_index } => {
259 write!(f, "bootstrap leg {at_index} could not bracket a root")
260 }
261 Self::InvalidInstrument { at_index, reason } => {
262 write!(f, "invalid instrument at index {at_index}: {reason}")
263 }
264 Self::Curve(e) => write!(f, "curve error during bootstrap: {e}"),
265 Self::Type(e) => write!(f, "type error during bootstrap: {e}"),
266 }
267 }
268}
269
270impl std::error::Error for BootstrapError {
271 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
272 match self {
273 Self::Curve(e) => Some(e),
274 Self::Type(e) => Some(e),
275 _ => None,
276 }
277 }
278}
279
280impl From<TypeError> for BootstrapError {
281 fn from(e: TypeError) -> Self {
282 Self::Type(e)
283 }
284}
285
286impl From<CurveError> for BootstrapError {
287 fn from(e: CurveError) -> Self {
288 Self::Curve(e)
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
299 fn type_error_display_invalid_date() {
300 let err = TypeError::InvalidDate {
301 year: 2023,
302 month: 2,
303 day: 30,
304 };
305 assert_eq!(format!("{err}"), "invalid calendar date: 2023-02-30");
306 }
307
308 #[test]
309 fn type_error_display_non_positive_range() {
310 let err = TypeError::NonPositiveRange;
311 assert!(format!("{err}").contains("positive"));
312 }
313
314 #[test]
315 fn type_error_display_non_finite() {
316 let err = TypeError::NonFinite { name: "rate" };
317 assert!(format!("{err}").contains("rate"));
318 assert!(format!("{err}").contains("finite"));
319 }
320
321 #[test]
322 fn type_error_display_invalid_tenor() {
323 let err = TypeError::InvalidTenor {
324 reason: "Business252 requires a calendar",
325 };
326 assert!(format!("{err}").contains("Business252"));
327 }
328
329 #[test]
330 fn type_error_is_error_trait() {
331 let err: &dyn std::error::Error = &TypeError::NonPositiveRange;
332 assert!(err.source().is_none());
333 }
334
335 #[test]
336 fn type_error_copy_eq_hash() {
337 let err = TypeError::NonFinite { name: "rate" };
338 let copy = err;
339 assert_eq!(err, copy);
340 let mut set = std::collections::HashSet::new();
342 set.insert(err);
343 assert!(set.contains(©));
344 }
345
346 #[test]
347 fn type_error_debug() {
348 assert!(format!("{:?}", TypeError::NonPositiveRange).contains("NonPositiveRange"));
349 }
350
351 #[test]
354 fn curve_error_display_all_variants() {
355 assert!(format!("{}", CurveError::TooFewNodes { found: 1 }).contains("two nodes"));
356 assert!(format!("{}", CurveError::NodesNotIncreasing { at_index: 4 }).contains('4'));
357 assert!(
358 format!(
359 "{}",
360 CurveError::NonPositiveDiscount {
361 at_index: 2,
362 value: -0.5,
363 }
364 )
365 .contains("-0.5")
366 );
367 assert!(format!("{}", CurveError::AnchorNotUnit).contains("anchor"));
368 assert!(format!("{}", CurveError::InvalidTime { t: -1.0 }).contains("-1"));
369 assert!(format!("{}", CurveError::DuplicateNode { t: 0.5 }).contains("0.5"));
370 assert!(
371 format!("{}", CurveError::Type(TypeError::NonPositiveRange)).contains("type error")
372 );
373 }
374
375 #[test]
376 fn curve_error_from_type_and_source() {
377 let te = TypeError::NonPositiveRange;
378 let ce: CurveError = te.into();
379 assert!(matches!(ce, CurveError::Type(_)));
380 let dyn_err: &dyn std::error::Error = &ce;
381 assert!(dyn_err.source().is_some());
382 }
383
384 #[test]
385 fn curve_error_no_source_for_plain_variants() {
386 let ce = CurveError::AnchorNotUnit;
387 let dyn_err: &dyn std::error::Error = &ce;
388 assert!(dyn_err.source().is_none());
389 }
390
391 #[test]
392 fn curve_error_copy_eq() {
393 let err = CurveError::TooFewNodes { found: 0 };
394 let copy = err;
395 assert_eq!(err, copy);
396 }
397
398 #[test]
399 fn curve_error_debug() {
400 assert!(format!("{:?}", CurveError::AnchorNotUnit).contains("AnchorNotUnit"));
401 }
402
403 #[test]
406 fn bootstrap_error_display_all_variants() {
407 assert!(format!("{}", BootstrapError::InstrumentsNotOrdered { at_index: 2 }).contains('2'));
408 assert!(format!("{}", BootstrapError::NonIncreasingAnchor { at_index: 5 }).contains('5'));
409 let m = format!(
410 "{}",
411 BootstrapError::LegDidNotConverge {
412 at_index: 3,
413 residual: 1.2e-9,
414 }
415 );
416 assert!(m.contains('3'));
417 assert!(m.contains("converge"));
418 assert!(format!("{}", BootstrapError::NoBracket { at_index: 7 }).contains('7'));
419 assert!(
420 format!(
421 "{}",
422 BootstrapError::InvalidInstrument {
423 at_index: 1,
424 reason: "negative rate",
425 }
426 )
427 .contains("negative rate")
428 );
429 assert!(
430 format!("{}", BootstrapError::Curve(CurveError::AnchorNotUnit)).contains("curve error")
431 );
432 assert!(
433 format!("{}", BootstrapError::Type(TypeError::NonPositiveRange)).contains("type error")
434 );
435 }
436
437 #[test]
438 fn bootstrap_error_from_type() {
439 let te = TypeError::NonPositiveRange;
440 let be: BootstrapError = te.into();
441 assert!(matches!(be, BootstrapError::Type(_)));
442 let dyn_err: &dyn std::error::Error = &be;
443 assert!(dyn_err.source().is_some());
444 }
445
446 #[test]
447 fn bootstrap_error_from_curve() {
448 let ce = CurveError::AnchorNotUnit;
449 let be: BootstrapError = ce.into();
450 assert!(matches!(be, BootstrapError::Curve(_)));
451 let dyn_err: &dyn std::error::Error = &be;
452 assert!(dyn_err.source().is_some());
453 }
454
455 #[test]
456 fn bootstrap_error_no_source_for_plain_variants() {
457 let be = BootstrapError::NoBracket { at_index: 0 };
458 let dyn_err: &dyn std::error::Error = &be;
459 assert!(dyn_err.source().is_none());
460 }
461
462 #[test]
463 fn bootstrap_error_copy_eq() {
464 let err = BootstrapError::InstrumentsNotOrdered { at_index: 0 };
465 let copy = err;
466 assert_eq!(err, copy);
467 }
468
469 #[test]
470 fn bootstrap_error_debug() {
471 assert!(format!("{:?}", BootstrapError::NoBracket { at_index: 0 }).contains("NoBracket"));
472 }
473
474 #[test]
475 fn bootstrap_error_chained_from_type_through_curve_is_not_automatic() {
476 let te = TypeError::NonPositiveRange;
479 let be: BootstrapError = te.into();
480 assert!(matches!(be, BootstrapError::Type(_)));
481 }
482}