use crate::error::GreeksError;
use optionstratlib_core::error::{DecimalError, OptionsError, PositionError};
use optionstratlib_core::model::ExpirationDateError;
use optionstratlib_core::model::PositiveError;
use rust_decimal::Decimal;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum PricingError {
#[error("Pricing method '{method}' failed: {reason}")]
MethodError {
method: String,
reason: String,
},
#[error("Pricing simulation failed: {reason}")]
SimulationError {
reason: String,
},
#[error("Invalid pricing engine: {reason}")]
InvalidEngine {
reason: String,
},
#[error(transparent)]
Greeks(#[from] GreeksError),
#[error(transparent)]
Options(#[from] OptionsError),
#[error(transparent)]
Position(#[from] PositionError),
#[error(transparent)]
Decimal(#[from] DecimalError),
#[error(transparent)]
ExpirationDate(#[from] ExpirationDateError),
#[error("delta adjustments are not applicable to single-leg {strategy} strategy")]
DeltaAdjustmentNotApplicable {
strategy: &'static str,
},
#[error("binomial pricing node `{node}` is missing")]
BinomialNodeMissing {
node: &'static str,
},
#[error("pricing sqrt failed for value {value}")]
SqrtFailure {
value: Decimal,
},
#[error(transparent)]
Positive(#[from] PositiveError),
#[error("Unsupported option type '{option_type}' for pricing method '{method}'")]
UnsupportedOptionType {
option_type: String,
method: String,
},
#[error("pricing non-finite {context}: {value}")]
NonFinite {
context: &'static str,
value: f64,
},
#[error("invalid pricing parameter `{parameter}` = {value}: {reason}")]
InvalidParameter {
parameter: &'static str,
value: Decimal,
reason: &'static str,
},
}
impl PricingError {
#[must_use]
#[cold]
#[inline(never)]
pub fn method_error(method: &str, reason: &str) -> Self {
PricingError::MethodError {
method: method.to_string(),
reason: reason.to_string(),
}
}
#[must_use]
#[cold]
#[inline(never)]
pub fn simulation_error(reason: &str) -> Self {
PricingError::SimulationError {
reason: reason.to_string(),
}
}
#[must_use]
#[cold]
#[inline(never)]
pub fn invalid_engine(reason: &str) -> Self {
PricingError::InvalidEngine {
reason: reason.to_string(),
}
}
#[must_use]
#[cold]
#[inline(never)]
pub fn other(reason: &str) -> Self {
PricingError::MethodError {
method: "pricing".to_string(),
reason: reason.to_string(),
}
}
#[must_use]
#[cold]
#[inline(never)]
pub fn unsupported_option_type(option_type: &str, method: &str) -> Self {
PricingError::UnsupportedOptionType {
option_type: option_type.to_string(),
method: method.to_string(),
}
}
#[cold]
#[inline(never)]
#[must_use]
pub fn non_finite(context: &'static str, value: f64) -> Self {
PricingError::NonFinite { context, value }
}
#[cold]
#[inline(never)]
#[must_use]
pub fn invalid_parameter(
parameter: &'static str,
value: Decimal,
reason: &'static str,
) -> Self {
PricingError::InvalidParameter {
parameter,
value,
reason,
}
}
}
pub type PricingResult<T> = Result<T, PricingError>;
impl From<PricingError> for OptionsError {
#[inline]
fn from(value: PricingError) -> Self {
Self::PricingError {
method: "unknown".to_string(),
reason: value.to_string(),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests_pricing_non_finite {
use super::*;
#[test]
fn test_pricing_error_converts_into_options_error() {
let pricing = PricingError::invalid_engine("bad engine");
let error: OptionsError = pricing.into();
assert!(matches!(error, OptionsError::PricingError { .. }));
}
#[test]
fn non_finite_constructor_nan() {
let err = PricingError::non_finite("pricing::bs::d1", f64::NAN);
match err {
PricingError::NonFinite { context, value } => {
assert_eq!(context, "pricing::bs::d1");
assert!(value.is_nan());
}
_ => panic!("expected NonFinite"),
}
}
#[test]
fn non_finite_display_includes_context_and_value() {
let err = PricingError::non_finite("pricing::mc::payoff", f64::INFINITY);
let msg = err.to_string();
assert!(msg.contains("pricing::mc::payoff"));
assert!(msg.contains("inf"));
}
#[test]
fn test_invalid_parameter_display_names_parameter_and_value() {
let err = PricingError::invalid_parameter(
"sigma_plus",
Decimal::ZERO,
"must be strictly positive",
);
let msg = err.to_string();
assert!(msg.contains("sigma_plus"));
assert!(msg.contains("must be strictly positive"));
}
#[test]
fn non_finite_neg_infinity() {
let err = PricingError::non_finite("pricing::kernel", f64::NEG_INFINITY);
match err {
PricingError::NonFinite { value, .. } => {
assert!(value.is_infinite() && value.is_sign_negative());
}
_ => panic!("expected NonFinite"),
}
}
}