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
196impl From<optionstratlib_core::error::ClockError> for SimulationError {
197 #[cold]
198 #[inline(never)]
199 fn from(err: optionstratlib_core::error::ClockError) -> Self {
200 Self::ExpirationDate(err.into())
201 }
202}
203
204#[cfg(test)]
205mod tests_typed_causes {
206 use super::*;
207 use optionstratlib_pricing::error::VolatilityError;
208
209 /// The conversion a simulation kernel uses when a volatility call fails
210 /// keeps the pricing cause, so a caller can match on it instead of
211 /// reading a message.
212 #[test]
213 fn test_a_volatility_failure_keeps_its_variant_through_the_conversion() {
214 let cause = VolatilityError::NumericalFailure {
215 reason: "constant_volatility: sqrt(variance) failed".to_string(),
216 };
217
218 let error = SimulationError::from(cause);
219
220 match error {
221 SimulationError::Volatility(inner) => match *inner {
222 VolatilityError::NumericalFailure { reason } => {
223 assert!(reason.contains("constant_volatility"), "{reason}");
224 }
225 other => panic!("the volatility variant was not preserved: {other:?}"),
226 },
227 other => panic!("expected SimulationError::Volatility, got {other:?}"),
228 }
229 }
230
231 /// A non-finite volatility keeps its structured payload rather than
232 /// becoming an invalid-parameters string.
233 #[test]
234 fn test_a_non_finite_volatility_keeps_its_payload() {
235 let cause = VolatilityError::NonFinite {
236 context: "ou_process",
237 value: f64::NAN,
238 };
239
240 match SimulationError::from(cause) {
241 SimulationError::Volatility(inner) => match *inner {
242 VolatilityError::NonFinite { context, value } => {
243 assert_eq!(context, "ou_process");
244 assert!(value.is_nan());
245 }
246 other => panic!("the payload was lost: {other:?}"),
247 },
248 other => panic!("expected SimulationError::Volatility, got {other:?}"),
249 }
250 }
251}