Skip to main content

optionstratlib_simulation/error/
simulation.rs

1//! Owned by `optionstratlib-simulation` (ADR-0001 D6, roadmap M1-14). Owns `SimulationError`; every variant wraps an error of this layer or of core and pricing below it.
2
3use optionstratlib_core::error::{DecimalError, OptionsError};
4use optionstratlib_core::model::ExpirationDateError;
5use optionstratlib_core::model::Positive;
6use optionstratlib_pricing::error::PricingError;
7use rust_decimal::Decimal;
8use thiserror::Error;
9
10/// Error type for simulation operations.
11///
12/// This enum represents the various errors that can occur during simulation operations,
13/// such as random walks, Monte Carlo simulations, and other stochastic processes.
14#[derive(Error, Debug)]
15pub enum SimulationError {
16    /// Error during walk generation.
17    #[error("Walk generation failed: {reason}")]
18    WalkError {
19        /// Detailed reason for the walk generation failure
20        reason: String,
21    },
22
23    /// Error due to invalid simulation parameters.
24    #[error("Invalid simulation parameters: {reason}")]
25    InvalidParameters {
26        /// Detailed reason for the invalid parameters
27        reason: String,
28    },
29
30    /// Error during step calculation.
31    #[error("Step calculation failed: {reason}")]
32    StepError {
33        /// Detailed reason for the step calculation failure
34        reason: String,
35    },
36
37    /// The walk type in the parameters does not match the generator being invoked.
38    ///
39    /// Raised from inside each `WalkTypeAble` implementation when the supplied
40    /// `walk_type` discriminator is not the one expected by the method.
41    #[error("invalid walk type: expected {expected}")]
42    InvalidWalkType {
43        /// Human-readable description of the expected walk type, e.g. `"Brownian"`.
44        expected: &'static str,
45    },
46
47    /// Autocorrelation parameter is outside the required `[-1, 1]` interval.
48    #[error("autocorrelation {value} must lie in [-1, 1]")]
49    InvalidAutocorrelation {
50        /// The offending autocorrelation value.
51        value: Decimal,
52    },
53
54    /// GARCH stationarity constraint `alpha + beta < 1` violated.
55    #[error("GARCH stationarity violated: alpha ({alpha}) + beta ({beta}) must be < 1")]
56    GarchStationarity {
57        /// The GARCH alpha coefficient.
58        alpha: Positive,
59        /// The GARCH beta coefficient.
60        beta: Positive,
61    },
62
63    /// Heston correlation `rho` is outside the valid `[-1, 1]` interval.
64    #[error("Heston correlation rho {rho} must lie in [-1, 1]")]
65    InvalidCorrelation {
66        /// The offending correlation value.
67        rho: Decimal,
68    },
69
70    /// Not enough historical price observations to generate the requested walk.
71    #[error("historical walk requires at least {required} observations, found {found}")]
72    InsufficientHistoricalData {
73        /// Minimum number of observations required.
74        required: usize,
75        /// Number of observations actually available.
76        found: usize,
77    },
78
79    /// Failed to convert the x-axis step index into a `Decimal`.
80    #[error("cannot convert x-axis step index to Decimal")]
81    IndexConversion,
82
83    /// The simulated expiration has already been reached, no further steps
84    /// can be generated.
85    #[error("cannot generate next step: expiration date already reached")]
86    ExpirationReached,
87
88    /// Decimal arithmetic error surfaced from pricing or Greek calculations.
89    #[error(transparent)]
90    Decimal(#[from] DecimalError),
91
92    /// Options domain error surfaced during simulation.
93    #[error(transparent)]
94    Options(#[from] OptionsError),
95
96    /// Pricing error surfaced during simulation.
97    #[error(transparent)]
98    Pricing(#[from] PricingError),
99
100    /// Expiration-date conversion error.
101    #[error(transparent)]
102    ExpirationDate(#[from] ExpirationDateError),
103
104    /// Positive value errors
105    #[error(transparent)]
106    PositiveError(#[from] optionstratlib_core::model::PositiveError),
107
108    /// A simulation kernel produced a non-finite `f64` value (`NaN` /
109    /// `±∞`) at an `f64` → `Decimal` boundary.
110    ///
111    /// Emitted by Brownian / geometric Brownian motion, Heston,
112    /// telegraph, and general random-walk path generators whenever
113    /// an intermediate `f64` would otherwise be silently cast into
114    /// `Decimal::ZERO`. `context` is a static call-site tag
115    /// following the same convention as
116    /// [`optionstratlib_core::error::DecimalError::Overflow`].
117    #[error("simulation non-finite {context}: {value}")]
118    NonFinite {
119        /// Static tag identifying the kernel and step that produced
120        /// the non-finite value.
121        context: &'static str,
122        /// The offending `f64` value (`NaN`, `+∞`, or `-∞`).
123        value: f64,
124    },
125    /// A volatility failure raised by the pricing capability this layer
126    /// depends on. Boxed because `VolatilityError` is the larger enum.
127    #[error(transparent)]
128    Volatility(Box<optionstratlib_pricing::error::VolatilityError>),
129}
130
131impl SimulationError {
132    /// Creates a new `WalkError` variant.
133    ///
134    /// # Arguments
135    /// * `reason` - Detailed reason for the walk generation failure
136    #[must_use]
137    #[cold]
138    #[inline(never)]
139    pub fn walk_error(reason: &str) -> Self {
140        SimulationError::WalkError {
141            reason: reason.to_string(),
142        }
143    }
144
145    /// Creates a new `InvalidParameters` variant.
146    ///
147    /// # Arguments
148    /// * `reason` - Detailed reason for the invalid parameters
149    #[must_use]
150    #[cold]
151    #[inline(never)]
152    pub fn invalid_parameters(reason: &str) -> Self {
153        SimulationError::InvalidParameters {
154            reason: reason.to_string(),
155        }
156    }
157
158    /// Creates a new `StepError` variant.
159    ///
160    /// # Arguments
161    /// * `reason` - Detailed reason for the step calculation failure
162    #[must_use]
163    #[cold]
164    #[inline(never)]
165    pub fn step_error(reason: &str) -> Self {
166        SimulationError::StepError {
167            reason: reason.to_string(),
168        }
169    }
170
171    /// Creates a [`SimulationError::NonFinite`] from a static call-site
172    /// tag and the offending `f64` value.
173    #[cold]
174    #[inline(never)]
175    #[must_use]
176    pub fn non_finite(context: &'static str, value: f64) -> Self {
177        SimulationError::NonFinite { context, value }
178    }
179}
180
181impl From<optionstratlib_pricing::error::VolatilityError> for SimulationError {
182    #[inline]
183    fn from(err: optionstratlib_pricing::error::VolatilityError) -> Self {
184        // Volatility is a pricing capability simulation depends on, so the
185        // cause stays typed. Only the market wrapper it used to route
186        // through was the problem, never the payload.
187        SimulationError::Volatility(Box::new(err))
188    }
189}
190
191/// Type alias for Results that may return a `SimulationError`.
192///
193/// This is a convenience type for functions that return simulation results.
194pub type SimulationResult<T> = Result<T, SimulationError>;
195
196#[cfg(test)]
197mod tests_typed_causes {
198    use super::*;
199    use optionstratlib_pricing::error::VolatilityError;
200
201    /// The conversion a simulation kernel uses when a volatility call fails
202    /// keeps the pricing cause, so a caller can match on it instead of
203    /// reading a message.
204    #[test]
205    fn test_a_volatility_failure_keeps_its_variant_through_the_conversion() {
206        let cause = VolatilityError::NumericalFailure {
207            reason: "constant_volatility: sqrt(variance) failed".to_string(),
208        };
209
210        let error = SimulationError::from(cause);
211
212        match error {
213            SimulationError::Volatility(inner) => match *inner {
214                VolatilityError::NumericalFailure { reason } => {
215                    assert!(reason.contains("constant_volatility"), "{reason}");
216                }
217                other => panic!("the volatility variant was not preserved: {other:?}"),
218            },
219            other => panic!("expected SimulationError::Volatility, got {other:?}"),
220        }
221    }
222
223    /// A non-finite volatility keeps its structured payload rather than
224    /// becoming an invalid-parameters string.
225    #[test]
226    fn test_a_non_finite_volatility_keeps_its_payload() {
227        let cause = VolatilityError::NonFinite {
228            context: "ou_process",
229            value: f64::NAN,
230        };
231
232        match SimulationError::from(cause) {
233            SimulationError::Volatility(inner) => match *inner {
234                VolatilityError::NonFinite { context, value } => {
235                    assert_eq!(context, "ou_process");
236                    assert!(value.is_nan());
237                }
238                other => panic!("the payload was lost: {other:?}"),
239            },
240            other => panic!("expected SimulationError::Volatility, got {other:?}"),
241        }
242    }
243}