use core::fmt;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ParamError {
NegativeSlope {
b: f64,
},
CorrelationOutOfRange {
rho: f64,
},
NonPositiveSigma {
sigma: f64,
},
NegativeMinVariance {
w_min: f64,
},
NonPositiveMaturity {
t: f64,
},
NegativeWeight {
weight: f64,
},
NegativeTotalVariance {
w: f64,
},
InvalidPhiParameter {
name: &'static str,
value: f64,
},
NonPositiveTheta {
theta: f64,
},
NonFinite {
name: &'static str,
},
}
impl fmt::Display for ParamError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NegativeSlope { b } => {
write!(f, "raw SVI slope b must be non-negative, got {b}")
}
Self::CorrelationOutOfRange { rho } => {
write!(f, "correlation rho must lie in (-1, 1), got {rho}")
}
Self::NonPositiveSigma { sigma } => {
write!(f, "raw SVI curvature sigma must be positive, got {sigma}")
}
Self::NegativeMinVariance { w_min } => {
write!(
f,
"minimum total variance must be non-negative, got w_min = {w_min}"
)
}
Self::NonPositiveMaturity { t } => {
write!(f, "maturity t must be positive, got {t}")
}
Self::NegativeWeight { weight } => {
write!(f, "quote weight must be non-negative, got {weight}")
}
Self::NegativeTotalVariance { w } => {
write!(f, "quoted total variance must be non-negative, got {w}")
}
Self::InvalidPhiParameter { name, value } => {
write!(f, "SSVI phi parameter {name} is out of range: {value}")
}
Self::NonPositiveTheta { theta } => {
write!(f, "SSVI ATM variance theta must be positive, got {theta}")
}
Self::NonFinite { name } => {
write!(f, "input {name} must be a finite number")
}
}
}
}
impl std::error::Error for ParamError {}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ConvertError {
JwHasNoRawPreimage {
beta: f64,
},
NegativeWingSlope {
name: &'static str,
value: f64,
},
NonPositiveAtmVariance {
w: f64,
},
DegenerateJw,
Param(ParamError),
}
impl fmt::Display for ConvertError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::JwHasNoRawPreimage { beta } => {
write!(
f,
"Jump-Wings tuple has no raw SVI pre-image: |beta| > 1, beta = {beta}"
)
}
Self::NegativeWingSlope { name, value } => {
write!(
f,
"Jump-Wings wing slope {name} must be non-negative, got {value}"
)
}
Self::NonPositiveAtmVariance { w } => {
write!(f, "Jump-Wings ATM total variance must be positive, got {w}")
}
Self::DegenerateJw => {
write!(
f,
"Jump-Wings tuple is degenerate: inverse map is indeterminate"
)
}
Self::Param(e) => write!(f, "converted slice is invalid: {e}"),
}
}
}
impl std::error::Error for ConvertError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Param(e) => Some(e),
_ => None,
}
}
}
impl From<ParamError> for ConvertError {
fn from(e: ParamError) -> Self {
Self::Param(e)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CalibrationError {
TooFewQuotes {
got: usize,
need: usize,
},
EmptyQuotes,
DidNotConverge {
iterations: usize,
residual: f64,
},
AllWeightsZero,
Param(ParamError),
}
impl fmt::Display for CalibrationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooFewQuotes { got, need } => {
write!(
f,
"too few quotes for calibration: got {got}, need at least {need}"
)
}
Self::EmptyQuotes => write!(f, "quote set is empty"),
Self::DidNotConverge {
iterations,
residual,
} => {
write!(
f,
"calibration did not converge after {iterations} iterations, residual = {residual}"
)
}
Self::AllWeightsZero => write!(f, "all fitting weights are zero"),
Self::Param(e) => write!(f, "calibrated slice is invalid: {e}"),
}
}
}
impl std::error::Error for CalibrationError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Param(e) => Some(e),
_ => None,
}
}
}
impl From<ParamError> for CalibrationError {
fn from(e: ParamError) -> Self {
Self::Param(e)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn param_error_display_negative_slope() {
let err = ParamError::NegativeSlope { b: -0.1 };
assert_eq!(
format!("{err}"),
"raw SVI slope b must be non-negative, got -0.1"
);
}
#[test]
fn param_error_display_correlation() {
let err = ParamError::CorrelationOutOfRange { rho: 1.5 };
assert!(format!("{err}").contains("1.5"));
}
#[test]
fn param_error_display_non_positive_sigma() {
let err = ParamError::NonPositiveSigma { sigma: 0.0 };
assert!(format!("{err}").contains("sigma"));
}
#[test]
fn param_error_display_negative_min_variance() {
let err = ParamError::NegativeMinVariance { w_min: -0.01 };
assert!(format!("{err}").contains("w_min"));
}
#[test]
fn param_error_display_remaining_variants() {
assert!(format!("{}", ParamError::NonPositiveMaturity { t: 0.0 }).contains("maturity"));
assert!(format!("{}", ParamError::NegativeWeight { weight: -1.0 }).contains("weight"));
assert!(
format!("{}", ParamError::NegativeTotalVariance { w: -0.1 }).contains("total variance")
);
assert!(
format!(
"{}",
ParamError::InvalidPhiParameter {
name: "eta",
value: -1.0
}
)
.contains("eta")
);
assert!(format!("{}", ParamError::NonPositiveTheta { theta: 0.0 }).contains("theta"));
assert!(format!("{}", ParamError::NonFinite { name: "k" }).contains("finite"));
}
#[test]
fn param_error_is_error_trait() {
let err: &dyn std::error::Error = &ParamError::NegativeSlope { b: -1.0 };
assert!(err.source().is_none());
}
#[test]
fn param_error_copy_eq() {
let err = ParamError::NonFinite { name: "x" };
let copy = err;
assert_eq!(err, copy);
}
#[test]
fn convert_error_display() {
let err = ConvertError::JwHasNoRawPreimage { beta: 1.4 };
assert!(format!("{err}").contains("1.4"));
let err = ConvertError::NegativeWingSlope {
name: "p_t",
value: -1.0,
};
assert!(format!("{err}").contains("p_t"));
assert!(format!("{}", ConvertError::DegenerateJw).contains("degenerate"));
assert!(
format!("{}", ConvertError::NonPositiveAtmVariance { w: -0.1 }).contains("positive")
);
}
#[test]
fn convert_error_from_param_and_source() {
let pe = ParamError::NegativeSlope { b: -1.0 };
let ce: ConvertError = pe.into();
assert!(matches!(ce, ConvertError::Param(_)));
let dyn_err: &dyn std::error::Error = &ce;
assert!(dyn_err.source().is_some());
}
#[test]
fn calibration_error_display() {
let err = CalibrationError::TooFewQuotes { got: 2, need: 5 };
let msg = format!("{err}");
assert!(msg.contains('2') && msg.contains('5'));
assert!(format!("{}", CalibrationError::EmptyQuotes).contains("empty"));
assert!(
format!(
"{}",
CalibrationError::DidNotConverge {
iterations: 100,
residual: 1e-3
}
)
.contains("converge")
);
assert!(format!("{}", CalibrationError::AllWeightsZero).contains("weights"));
}
#[test]
fn calibration_error_from_param_and_source() {
let pe = ParamError::NonPositiveSigma { sigma: 0.0 };
let ce: CalibrationError = pe.into();
assert!(matches!(ce, CalibrationError::Param(_)));
let dyn_err: &dyn std::error::Error = &ce;
assert!(dyn_err.source().is_some());
}
#[test]
fn errors_debug() {
assert!(format!("{:?}", ParamError::NonFinite { name: "k" }).contains("NonFinite"));
assert!(format!("{:?}", ConvertError::DegenerateJw).contains("Degenerate"));
assert!(format!("{:?}", CalibrationError::EmptyQuotes).contains("Empty"));
}
}