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}