Skip to main content

optionstratlib_pricing/error/
pricing.rs

1//! Target crate (ADR-0001 D6, roadmap M1-14): **pricing**. Owns `PricingError`.
2
3use crate::error::GreeksError;
4use optionstratlib_core::error::{DecimalError, OptionsError, PositionError};
5use optionstratlib_core::model::ExpirationDateError;
6use optionstratlib_core::model::PositiveError;
7use rust_decimal::Decimal;
8use thiserror::Error;
9
10/// Error type for option pricing operations.
11///
12/// This enum represents the various errors that can occur during option pricing,
13/// providing domain-specific error handling for different pricing scenarios.
14#[derive(Error, Debug)]
15pub enum PricingError {
16    /// Error from a specific pricing method (e.g., Black-Scholes, Binomial).
17    #[error("Pricing method '{method}' failed: {reason}")]
18    MethodError {
19        /// Name of the pricing method that failed
20        method: String,
21        /// Detailed reason for the failure
22        reason: String,
23    },
24
25    /// Error during Monte Carlo simulation.
26    #[error("Pricing simulation failed: {reason}")]
27    SimulationError {
28        /// Detailed reason for the simulation failure
29        reason: String,
30    },
31
32    /// Error due to invalid pricing engine configuration.
33    #[error("Invalid pricing engine: {reason}")]
34    InvalidEngine {
35        /// Detailed reason for the invalid engine
36        reason: String,
37    },
38
39    /// Error from Greeks calculations.
40    #[error(transparent)]
41    Greeks(#[from] GreeksError),
42
43    /// Error from Options operations.
44    #[error(transparent)]
45    Options(#[from] OptionsError),
46
47    /// Error from Position operations.
48    #[error(transparent)]
49    Position(#[from] PositionError),
50
51    /// Error from Decimal operations.
52    #[error(transparent)]
53    Decimal(#[from] DecimalError),
54
55    /// Expiration-date conversion error surfaced during pricing.
56    #[error(transparent)]
57    ExpirationDate(#[from] ExpirationDateError),
58
59    /// A delta adjustment was requested on a strategy that does not support it.
60    #[error("delta adjustments are not applicable to single-leg {strategy} strategy")]
61    DeltaAdjustmentNotApplicable {
62        /// Name of the strategy, e.g. `"LongCall"`, `"ShortPut"`.
63        strategy: &'static str,
64    },
65
66    /// A required intermediate value on a binomial lattice or pricing kernel
67    /// was missing (typically a node that should have been populated by an
68    /// earlier induction step).
69    #[error("binomial pricing node `{node}` is missing")]
70    BinomialNodeMissing {
71        /// Identifier of the missing node, e.g. `"S_k"`, `"b"`.
72        node: &'static str,
73    },
74
75    /// A square-root computation inside a pricing kernel failed (the operand
76    /// was negative or not representable — e.g. a negative discriminant in a
77    /// closed-form solver).
78    #[error("pricing sqrt failed for value {value}")]
79    SqrtFailure {
80        /// The value for which the square root could not be computed.
81        ///
82        /// Stored as `Decimal` so negative operands are representable.
83        value: Decimal,
84    },
85
86    /// Error from Positive operations.
87    #[error(transparent)]
88    Positive(#[from] PositiveError),
89
90    /// Error for unsupported option types.
91    #[error("Unsupported option type '{option_type}' for pricing method '{method}'")]
92    UnsupportedOptionType {
93        /// The option type that is not supported
94        option_type: String,
95        /// The pricing method that does not support this option type
96        method: String,
97    },
98
99    /// A pricing kernel produced a non-finite `f64` value (`NaN` /
100    /// `±∞`) at an `f64` → `Decimal` boundary.
101    ///
102    /// Emitted wherever a Black-Scholes style closed-form, a
103    /// numerical integrator, or a Monte-Carlo payoff computes an
104    /// intermediate `f64` that would otherwise be wrapped in
105    /// `Decimal::from_f64(..)` and silently become `Decimal::ZERO`
106    /// or saturate. `context` is a static call-site tag following the
107    /// same convention as [`optionstratlib_core::error::DecimalError::Overflow`]
108    /// (for example `"pricing::bs::call::d1"` or
109    /// `"pricing::monte_carlo::payoff::cast"`), so the failing
110    /// kernel is identifiable without a stack trace.
111    #[error("pricing non-finite {context}: {value}")]
112    NonFinite {
113        /// Static tag identifying the kernel and step that produced
114        /// the non-finite value.
115        context: &'static str,
116        /// The offending `f64` value (`NaN`, `+∞`, or `-∞`).
117        value: f64,
118    },
119
120    /// A model parameter passed to a pricing kernel lies outside its
121    /// admissible domain, for example a regime volatility that is not
122    /// strictly positive or a negative transition rate of the telegraph
123    /// pricer.
124    #[error("invalid pricing parameter `{parameter}` = {value}: {reason}")]
125    InvalidParameter {
126        /// Name of the offending parameter (for example `"sigma_plus"`).
127        parameter: &'static str,
128        /// The rejected value.
129        value: Decimal,
130        /// Why the value is rejected.
131        reason: &'static str,
132    },
133}
134
135impl PricingError {
136    /// Creates a new `MethodError` variant.
137    ///
138    /// # Arguments
139    /// * `method` - Name of the pricing method that failed
140    /// * `reason` - Detailed reason for the failure
141    #[must_use]
142    #[cold]
143    #[inline(never)]
144    pub fn method_error(method: &str, reason: &str) -> Self {
145        PricingError::MethodError {
146            method: method.to_string(),
147            reason: reason.to_string(),
148        }
149    }
150
151    /// Creates a new `SimulationError` variant.
152    ///
153    /// # Arguments
154    /// * `reason` - Detailed reason for the simulation failure
155    #[must_use]
156    #[cold]
157    #[inline(never)]
158    pub fn simulation_error(reason: &str) -> Self {
159        PricingError::SimulationError {
160            reason: reason.to_string(),
161        }
162    }
163
164    /// Creates a new `InvalidEngine` variant.
165    ///
166    /// # Arguments
167    /// * `reason` - Detailed reason for the invalid engine
168    #[must_use]
169    #[cold]
170    #[inline(never)]
171    pub fn invalid_engine(reason: &str) -> Self {
172        PricingError::InvalidEngine {
173            reason: reason.to_string(),
174        }
175    }
176
177    /// Creates a typed `MethodError` with `method = "pricing"` as a lightweight
178    /// replacement for the former `OtherError` catch-all. Prefer
179    /// [`PricingError::method_error`] with a specific method name when known.
180    #[must_use]
181    #[cold]
182    #[inline(never)]
183    pub fn other(reason: &str) -> Self {
184        PricingError::MethodError {
185            method: "pricing".to_string(),
186            reason: reason.to_string(),
187        }
188    }
189
190    /// Creates a new `UnsupportedOptionType` variant.
191    ///
192    /// # Arguments
193    /// * `option_type` - The option type that is not supported
194    /// * `method` - The pricing method that does not support this option type
195    #[must_use]
196    #[cold]
197    #[inline(never)]
198    pub fn unsupported_option_type(option_type: &str, method: &str) -> Self {
199        PricingError::UnsupportedOptionType {
200            option_type: option_type.to_string(),
201            method: method.to_string(),
202        }
203    }
204
205    /// Creates a [`PricingError::NonFinite`] from a static call-site
206    /// tag and the offending `f64` value.
207    ///
208    /// Intended to be used at `f64` → `Decimal` boundaries inside
209    /// pricing kernels, as a thin constructor paired with an
210    /// `if !value.is_finite() { .. }` guard.
211    #[cold]
212    #[inline(never)]
213    #[must_use]
214    pub fn non_finite(context: &'static str, value: f64) -> Self {
215        PricingError::NonFinite { context, value }
216    }
217
218    /// Creates a [`PricingError::InvalidParameter`] naming the rejected
219    /// parameter, its value and the domain it violates.
220    #[cold]
221    #[inline(never)]
222    #[must_use]
223    pub fn invalid_parameter(
224        parameter: &'static str,
225        value: Decimal,
226        reason: &'static str,
227    ) -> Self {
228        PricingError::InvalidParameter {
229            parameter,
230            value,
231            reason,
232        }
233    }
234}
235
236/// Type alias for Results that may return a `PricingError`.
237///
238/// This is a convenience type for functions that return pricing results.
239pub type PricingResult<T> = Result<T, PricingError>;
240
241// Conversions whose SOURCE error is owned by this layer and whose target
242// sits in a lower layer. They live here (ADR-0001 D6, M1-14) so that the
243// lower layer's error file never names a higher one.
244
245impl From<PricingError> for OptionsError {
246    #[inline]
247    fn from(value: PricingError) -> Self {
248        Self::PricingError {
249            method: "unknown".to_string(),
250            reason: value.to_string(),
251        }
252    }
253}
254
255#[cfg(test)]
256#[allow(clippy::unwrap_used)]
257mod tests_pricing_non_finite {
258    use super::*;
259
260    #[test]
261    fn test_pricing_error_converts_into_options_error() {
262        let pricing = PricingError::invalid_engine("bad engine");
263        let error: OptionsError = pricing.into();
264        assert!(matches!(error, OptionsError::PricingError { .. }));
265    }
266
267    #[test]
268    fn non_finite_constructor_nan() {
269        let err = PricingError::non_finite("pricing::bs::d1", f64::NAN);
270        match err {
271            PricingError::NonFinite { context, value } => {
272                assert_eq!(context, "pricing::bs::d1");
273                assert!(value.is_nan());
274            }
275            _ => panic!("expected NonFinite"),
276        }
277    }
278
279    #[test]
280    fn non_finite_display_includes_context_and_value() {
281        let err = PricingError::non_finite("pricing::mc::payoff", f64::INFINITY);
282        let msg = err.to_string();
283        assert!(msg.contains("pricing::mc::payoff"));
284        assert!(msg.contains("inf"));
285    }
286
287    #[test]
288    fn test_invalid_parameter_display_names_parameter_and_value() {
289        let err = PricingError::invalid_parameter(
290            "sigma_plus",
291            Decimal::ZERO,
292            "must be strictly positive",
293        );
294        let msg = err.to_string();
295        assert!(msg.contains("sigma_plus"));
296        assert!(msg.contains("must be strictly positive"));
297    }
298
299    #[test]
300    fn non_finite_neg_infinity() {
301        let err = PricingError::non_finite("pricing::kernel", f64::NEG_INFINITY);
302        match err {
303            PricingError::NonFinite { value, .. } => {
304                assert!(value.is_infinite() && value.is_sign_negative());
305            }
306            _ => panic!("expected NonFinite"),
307        }
308    }
309}