use optionstratlib_core::error::{DecimalError, OptionsError};
use optionstratlib_core::model::ExpirationDateError;
use optionstratlib_core::model::Positive;
use optionstratlib_pricing::error::PricingError;
use rust_decimal::Decimal;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum SimulationError {
#[error("Walk generation failed: {reason}")]
WalkError {
reason: String,
},
#[error("Invalid simulation parameters: {reason}")]
InvalidParameters {
reason: String,
},
#[error("Step calculation failed: {reason}")]
StepError {
reason: String,
},
#[error("invalid walk type: expected {expected}")]
InvalidWalkType {
expected: &'static str,
},
#[error("autocorrelation {value} must lie in [-1, 1]")]
InvalidAutocorrelation {
value: Decimal,
},
#[error("GARCH stationarity violated: alpha ({alpha}) + beta ({beta}) must be < 1")]
GarchStationarity {
alpha: Positive,
beta: Positive,
},
#[error("Heston correlation rho {rho} must lie in [-1, 1]")]
InvalidCorrelation {
rho: Decimal,
},
#[error("historical walk requires at least {required} observations, found {found}")]
InsufficientHistoricalData {
required: usize,
found: usize,
},
#[error("cannot convert x-axis step index to Decimal")]
IndexConversion,
#[error("cannot generate next step: expiration date already reached")]
ExpirationReached,
#[error(transparent)]
Decimal(#[from] DecimalError),
#[error(transparent)]
Options(#[from] OptionsError),
#[error(transparent)]
Pricing(#[from] PricingError),
#[error(transparent)]
ExpirationDate(#[from] ExpirationDateError),
#[error(transparent)]
PositiveError(#[from] optionstratlib_core::model::PositiveError),
#[error("simulation non-finite {context}: {value}")]
NonFinite {
context: &'static str,
value: f64,
},
#[error(transparent)]
Volatility(Box<optionstratlib_pricing::error::VolatilityError>),
}
impl SimulationError {
#[must_use]
#[cold]
#[inline(never)]
pub fn walk_error(reason: &str) -> Self {
SimulationError::WalkError {
reason: reason.to_string(),
}
}
#[must_use]
#[cold]
#[inline(never)]
pub fn invalid_parameters(reason: &str) -> Self {
SimulationError::InvalidParameters {
reason: reason.to_string(),
}
}
#[must_use]
#[cold]
#[inline(never)]
pub fn step_error(reason: &str) -> Self {
SimulationError::StepError {
reason: reason.to_string(),
}
}
#[cold]
#[inline(never)]
#[must_use]
pub fn non_finite(context: &'static str, value: f64) -> Self {
SimulationError::NonFinite { context, value }
}
}
impl From<optionstratlib_pricing::error::VolatilityError> for SimulationError {
#[inline]
fn from(err: optionstratlib_pricing::error::VolatilityError) -> Self {
SimulationError::Volatility(Box::new(err))
}
}
pub type SimulationResult<T> = Result<T, SimulationError>;
#[cfg(test)]
mod tests_typed_causes {
use super::*;
use optionstratlib_pricing::error::VolatilityError;
#[test]
fn test_a_volatility_failure_keeps_its_variant_through_the_conversion() {
let cause = VolatilityError::NumericalFailure {
reason: "constant_volatility: sqrt(variance) failed".to_string(),
};
let error = SimulationError::from(cause);
match error {
SimulationError::Volatility(inner) => match *inner {
VolatilityError::NumericalFailure { reason } => {
assert!(reason.contains("constant_volatility"), "{reason}");
}
other => panic!("the volatility variant was not preserved: {other:?}"),
},
other => panic!("expected SimulationError::Volatility, got {other:?}"),
}
}
#[test]
fn test_a_non_finite_volatility_keeps_its_payload() {
let cause = VolatilityError::NonFinite {
context: "ou_process",
value: f64::NAN,
};
match SimulationError::from(cause) {
SimulationError::Volatility(inner) => match *inner {
VolatilityError::NonFinite { context, value } => {
assert_eq!(context, "ou_process");
assert!(value.is_nan());
}
other => panic!("the payload was lost: {other:?}"),
},
other => panic!("expected SimulationError::Volatility, got {other:?}"),
}
}
}