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}