use optionstratlib_core::error::DecimalError;
use optionstratlib_core::model::{Positive, PositiveError};
use rust_decimal::Decimal;
use thiserror::Error;
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum SviError {
#[error("invalid svi parameter {parameter} = {value}: {reason}")]
InvalidParameter {
parameter: &'static str,
value: Decimal,
reason: &'static str,
},
#[error("svi minimum total variance {minimum} is negative")]
NegativeMinimumVariance {
minimum: Decimal,
},
#[error("svi forward must be positive")]
ZeroForward,
#[error("svi time to expiry must be positive")]
ZeroTimeToExpiry,
#[error("svi strike must be positive, got {strike}")]
ZeroStrike {
strike: Positive,
},
#[error("svi total variance {total_variance} at log moneyness {log_moneyness} is negative")]
NegativeTotalVariance {
log_moneyness: Decimal,
total_variance: Decimal,
},
#[error("invalid svi calibration config {parameter} = {value}: {reason}")]
InvalidConfig {
parameter: &'static str,
value: Decimal,
reason: &'static str,
},
#[error("invalid svi observation at index {index}: {reason}")]
InvalidObservation {
index: usize,
reason: &'static str,
},
#[error("svi calibration needs at least {required} weighted observations, got {got}")]
TooFewObservations {
got: usize,
required: usize,
},
#[error("svi observation weights must have a positive sum")]
NonPositiveTotalWeight,
#[error("svi calibration needs {required} distinct strikes, got {distinct}")]
InsufficientDistinctStrikes {
distinct: usize,
required: usize,
},
#[error(
"svi calibration did not converge: {iterations} iterations, limit {limit} for each start"
)]
NoConvergence {
iterations: u64,
limit: u64,
},
#[error("svi calibration found no feasible parameters: {reason}")]
NoFeasibleFit {
reason: String,
},
#[error(
"svi fit relative rmse {relative_rmse} exceeds the limit {limit} (rmse {rmse} in implied volatility)"
)]
RmseAboveLimit {
relative_rmse: Positive,
limit: Positive,
rmse: Positive,
},
#[error("svi non-finite {context}: {value}")]
NonFinite {
context: &'static str,
value: f64,
},
#[error(transparent)]
Decimal(#[from] DecimalError),
#[error(transparent)]
Positive(#[from] PositiveError),
}
impl SviError {
#[cold]
#[inline(never)]
#[must_use]
pub fn non_finite(context: &'static str, value: f64) -> Self {
SviError::NonFinite { context, value }
}
}
#[cfg(test)]
mod tests_svi_error {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_svi_error_rmse_above_limit_names_all_three_values() {
let error = SviError::RmseAboveLimit {
relative_rmse: Positive::new_decimal(dec!(0.15)).unwrap(),
limit: Positive::new_decimal(dec!(0.03)).unwrap(),
rmse: Positive::new_decimal(dec!(0.0173)).unwrap(),
};
let message = error.to_string();
assert!(message.contains("0.15"), "{message}");
assert!(message.contains("0.03"), "{message}");
assert!(message.contains("0.0173"), "{message}");
}
#[test]
fn test_svi_error_invalid_config_names_the_field_and_value() {
let message = SviError::InvalidConfig {
parameter: "tolerance",
value: dec!(1),
reason: "must not exceed 0.001",
}
.to_string();
assert!(message.contains("tolerance = 1"), "{message}");
}
#[test]
fn test_svi_error_non_finite_keeps_context_and_value() {
let error = SviError::non_finite("svi::kernel::objective", f64::INFINITY);
assert!(matches!(
error,
SviError::NonFinite {
context: "svi::kernel::objective",
value
} if value == f64::INFINITY
));
}
#[test]
fn test_svi_error_decimal_converts_with_from() {
let error: SviError = DecimalError::arithmetic_error("svi", "boom").into();
assert!(matches!(error, SviError::Decimal(_)));
}
#[test]
fn test_svi_error_no_convergence_names_iterations_and_limit() {
let message = SviError::NoConvergence {
iterations: 40,
limit: 2,
}
.to_string();
assert!(message.contains("40"), "{message}");
assert!(message.contains("limit 2"), "{message}");
}
}