optionstratlib_core/error/options.rs
1//! # Options Error Module
2//!
3//! This module provides error handling for financial options operations.
4//! It defines a custom error type `OptionsError` that encapsulates various error conditions
5//! that can occur during options trading and calculations.
6//!
7//! ## Error Types
8//!
9//! - `ValidationError`: For errors in option parameter validation
10//! - `PricingError`: For errors during price calculation
11//! - `GreeksCalculationError`: For errors when calculating Greeks
12//! - `TimeError`: For errors related to time calculations
13//! - `PayoffError`: For errors in payoff calculations
14//! - `UpdateError`: For errors during option data updates
15//!
16//! ## Usage
17//!
18//! ```rust
19//! use optionstratlib_core::error::OptionsError;
20//!
21//! // Create a validation error
22//! let error = OptionsError::validation_error("strike_price", "must be positive");
23//!
24//! // Create a pricing error
25//! let error = OptionsError::pricing_error("black_scholes", "invalid volatility");
26//!
27//! // Use with Result
28//! type Result<T> = std::result::Result<T, OptionsError>;
29//! ```
30//!
31//! ## Features
32//!
33//! - Implements standard Error trait
34//! - Provides detailed error context
35//! - Supports conversion from standard error types
36//! - Includes helper methods for error creation
37//!
38//! ## Error Conversion
39//!
40//! The module converts from typed core errors:
41//! - `DecimalError` via `#[from]`
42//! - `ExpirationDateError` via `#[from]`
43//!
44//! Higher layers own the conversions from their own errors; the pricing
45//! layer, for instance, implements `From<PricingError> for OptionsError`.
46//!
47//! ## Examples
48//!
49//! ```rust
50//! use optionstratlib_core::error::{OptionsError, OptionsResult};
51//!
52//! fn validate_strike_price(price: f64) -> OptionsResult<f64> {
53//! if price <= 0.0 {
54//! return Err(OptionsError::validation_error(
55//! "strike_price",
56//! "must be positive"
57//! ));
58//! }
59//! Ok(price)
60//! }
61//! ```
62//!
63//! Target crate (ADR-0001 D6, roadmap M1-14): **core**. Owns `OptionsError`.
64
65use crate::error::DecimalError;
66use expiration_date::error::ExpirationDateError;
67use thiserror::Error;
68
69/// Custom errors that can occur during Options operations
70///
71/// This enum provides a structured error system for handling various failure scenarios
72/// that may arise during option trading operations, calculations, and data management.
73/// Each variant represents a specific category of error with contextual information
74/// to help with debugging and error handling.
75///
76/// # Variants
77///
78/// * `ValidationError` - Errors that occur when validating option parameters
79/// such as strike prices, expiration dates, or option styles.
80///
81/// * `PricingError` - Errors that occur during option price calculation
82/// using various pricing models like Black-Scholes, Binomial, etc.
83///
84/// * `GreeksCalculationError` - Errors that occur when calculating option Greeks
85/// (delta, gamma, theta, vega, rho, etc.) which measure option price sensitivities.
86///
87/// * `TimeError` - Errors related to time calculations, such as determining
88/// days to expiration, time decay, or handling calendar adjustments.
89///
90/// * `PayoffError` - Errors that occur when calculating potential payoffs
91/// for options at different price points or expiration scenarios.
92///
93/// * `UpdateError` - Errors that occur when attempting to update option data
94/// or parameters in an existing option object.
95///
96/// * `InvalidStepCount` - Invalid pricing step count (typed replacement for the former `OtherError`).
97/// * `ImpliedVolatilityInvariant` - Implied-volatility invariant breach.
98///
99/// # Usage
100///
101/// This error type is typically returned in Result objects from functions that
102/// perform operations on option contracts, pricing calculations, or option strategy
103/// analysis where various error conditions need to be handled.
104#[derive(Error, Debug)]
105pub enum OptionsError {
106 /// Error when validating option parameters
107 ///
108 /// Used when input parameters for option contracts fail validation.
109 #[error("Validation error for field '{field}': {reason}")]
110 ValidationError {
111 /// The field name that failed validation
112 field: String,
113 /// Detailed explanation of the validation failure
114 reason: String,
115 },
116
117 /// Error during price calculation
118 ///
119 /// Used when an option pricing algorithm encounters problems.
120 #[error("Pricing error using method '{method}': {reason}")]
121 PricingError {
122 /// The pricing method that failed (e.g., "Black-Scholes", "Binomial")
123 method: String,
124 /// Detailed explanation of the pricing calculation failure
125 reason: String,
126 },
127
128 /// Error when calculating greeks
129 ///
130 /// Used when calculations for option sensitivities (Greeks) fail.
131 #[error("Error calculating greek '{greek}': {reason}")]
132 GreeksCalculationError {
133 /// The specific Greek that failed to calculate (delta, gamma, theta, etc.)
134 greek: String,
135 /// Detailed explanation of the Greek calculation failure
136 reason: String,
137 },
138
139 /// Error when dealing with time calculations
140 ///
141 /// Used for failures in time-related calculations like time to expiry.
142 #[error("Time calculation error in '{operation}': {reason}")]
143 TimeError {
144 /// The time-related operation that failed
145 operation: String,
146 /// Detailed explanation of the time calculation failure
147 reason: String,
148 },
149
150 /// Error when performing payoff calculations
151 ///
152 /// Used when potential profit/loss calculations for options fail.
153 #[error("Payoff calculation error: {reason}")]
154 PayoffError {
155 /// Detailed explanation of the payoff calculation failure
156 reason: String,
157 },
158
159 /// Error during option data updates
160 ///
161 /// Used when attempts to update option parameters or data fail.
162 #[error("Update error for field '{field}': {reason}")]
163 UpdateError {
164 /// The field that failed to update
165 field: String,
166 /// Detailed explanation of the update failure
167 reason: String,
168 },
169
170 /// The caller requested a pricing operation with zero tree steps, which
171 /// is structurally invalid.
172 #[error("invalid step count: {operation} requires at least one step")]
173 InvalidStepCount {
174 /// Name of the operation that rejected the step count (e.g. `"binomial"`).
175 operation: &'static str,
176 },
177
178 /// A numerical invariant was breached while constructing a valid implied
179 /// volatility value from two non-negative bounds.
180 #[error("implied volatility invariant breached: {reason}")]
181 ImpliedVolatilityInvariant {
182 /// Detailed explanation of which invariant was breached.
183 reason: String,
184 },
185
186 /// Error when DecimalError occurs
187 #[error(transparent)]
188 Decimal(#[from] DecimalError),
189
190 /// Expiration-date conversion error surfaced during options operations.
191 #[error(transparent)]
192 ExpirationDate(#[from] ExpirationDateError),
193}
194
195/// A specialized result type for operations related to Options calculations and processing.
196///
197/// This type alias simplifies error handling for functions that can fail with various
198/// options-specific errors. It uses the `OptionsError` enum to provide structured
199/// error information about validation failures, pricing issues, Greeks calculations,
200/// time-related problems, and other option-specific errors.
201///
202/// # Type Parameters
203///
204/// * `T` - The success type that will be returned if the operation succeeds.
205///
206/// # Examples
207///
208/// ```rust
209/// use optionstratlib_core::error::{OptionsResult, OptionsError};
210///
211/// fn calculate_call_price(strike: f64, spot: f64) -> OptionsResult<f64> {
212/// if strike <= 0.0 {
213/// return Err(OptionsError::ValidationError {
214/// field: "strike".to_string(),
215/// reason: "Strike price must be positive".to_string()
216/// });
217/// }
218///
219/// // Calculation logic would go here
220/// let price = 0.0; // Placeholder
221/// Ok(price)
222/// }
223/// ```
224///
225/// # Usage Context
226///
227/// This result type is commonly used throughout the library for:
228///
229/// * Option pricing calculations
230/// * Parameter validation
231/// * Greeks calculations
232/// * Expiration and time value calculations
233/// * Option payoff analysis
234///
235/// See `OptionsError` for the full variant list.
236pub type OptionsResult<T> = Result<T, OptionsError>;
237
238/// Helper methods for creating common options errors.
239///
240/// This implementation provides convenient factory methods for creating different
241/// variants of `OptionsError` without having to manually construct the enum variants.
242/// Each method corresponds to a specific error type and properly formats the error fields.
243///
244/// # Methods
245///
246/// * `validation_error` - Creates an error for parameter validation failures
247/// * `pricing_error` - Creates an error for pricing calculation issues
248/// * `greeks_error` - Creates an error for problems with Greeks calculations
249/// * `time_error` - Creates an error for time-related calculations
250/// * `payoff_error` - Creates an error for payoff calculation problems
251/// * `update_error` - Creates an error for option data update issues
252///
253/// # Examples
254///
255/// ```
256/// use optionstratlib_core::error::OptionsError;
257/// let error = OptionsError::validation_error("strike_price", "must be positive");
258///
259/// // Create a pricing error
260/// let error = OptionsError::pricing_error("black_scholes", "invalid volatility input");
261/// ```
262impl OptionsError {
263 /// Creates a validation error with the specified field name and reason.
264 ///
265 /// This method is used when option parameters fail validation checks.
266 ///
267 /// # Parameters
268 ///
269 /// * `field` - The name of the field that failed validation
270 /// * `reason` - The reason why validation failed
271 ///
272 /// # Returns
273 ///
274 /// An `OptionsError::ValidationError` variant with formatted fields
275 #[must_use]
276 #[cold]
277 #[inline(never)]
278 pub fn validation_error(field: &str, reason: &str) -> Self {
279 OptionsError::ValidationError {
280 field: field.to_string(),
281 reason: reason.to_string(),
282 }
283 }
284
285 /// Creates a pricing error with the specified pricing method and reason.
286 ///
287 /// This method is used when an error occurs during option price calculation.
288 ///
289 /// # Parameters
290 ///
291 /// * `method` - The name of the pricing method that encountered an error
292 /// * `reason` - The description of what went wrong
293 ///
294 /// # Returns
295 ///
296 /// An `OptionsError::PricingError` variant with formatted fields
297 #[must_use]
298 #[cold]
299 #[inline(never)]
300 pub fn pricing_error(method: &str, reason: &str) -> Self {
301 OptionsError::PricingError {
302 method: method.to_string(),
303 reason: reason.to_string(),
304 }
305 }
306
307 /// Creates a Greeks calculation error with the specified Greek name and reason.
308 ///
309 /// This method is used when an error occurs during the calculation of option Greeks
310 /// (delta, gamma, theta, vega, etc.).
311 ///
312 /// # Parameters
313 ///
314 /// * `greek` - The name of the Greek calculation that failed
315 /// * `reason` - The description of what went wrong
316 ///
317 /// # Returns
318 ///
319 /// An `OptionsError::GreeksCalculationError` variant with formatted fields
320 #[must_use]
321 #[cold]
322 #[inline(never)]
323 pub fn greeks_error(greek: &str, reason: &str) -> Self {
324 OptionsError::GreeksCalculationError {
325 greek: greek.to_string(),
326 reason: reason.to_string(),
327 }
328 }
329
330 /// Creates a time calculation error with the specified operation and reason.
331 ///
332 /// This method is used when an error occurs during time-related calculations,
333 /// such as time to expiration, day count conventions, or calendar adjustments.
334 ///
335 /// # Parameters
336 ///
337 /// * `operation` - The name of the time operation that failed
338 /// * `reason` - The description of what went wrong
339 ///
340 /// # Returns
341 ///
342 /// An `OptionsError::TimeError` variant with formatted fields
343 #[must_use]
344 #[cold]
345 #[inline(never)]
346 pub fn time_error(operation: &str, reason: &str) -> Self {
347 OptionsError::TimeError {
348 operation: operation.to_string(),
349 reason: reason.to_string(),
350 }
351 }
352
353 /// Creates a payoff calculation error with the specified reason.
354 ///
355 /// This method is used when an error occurs during the calculation of option payoffs.
356 ///
357 /// # Parameters
358 ///
359 /// * `reason` - The description of what went wrong
360 ///
361 /// # Returns
362 ///
363 /// An `OptionsError::PayoffError` variant with formatted reason
364 #[must_use]
365 #[cold]
366 #[inline(never)]
367 pub fn payoff_error(reason: &str) -> Self {
368 OptionsError::PayoffError {
369 reason: reason.to_string(),
370 }
371 }
372
373 /// Creates an update error with the specified field and reason.
374 ///
375 /// This method is used when an error occurs during the update of option parameters
376 /// or other option data.
377 ///
378 /// # Parameters
379 ///
380 /// * `field` - The name of the field that failed to update
381 /// * `reason` - The description of what went wrong
382 ///
383 /// # Returns
384 ///
385 /// An `OptionsError::UpdateError` variant with formatted fields
386 #[must_use]
387 #[cold]
388 #[inline(never)]
389 pub fn update_error(field: &str, reason: &str) -> Self {
390 OptionsError::UpdateError {
391 field: field.to_string(),
392 reason: reason.to_string(),
393 }
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 #[test]
402 fn test_validation_error_creation() {
403 let error = OptionsError::validation_error("price", "must be positive");
404 match error {
405 OptionsError::ValidationError { field, reason } => {
406 assert_eq!(field, "price");
407 assert_eq!(reason, "must be positive");
408 }
409 _ => panic!("Expected ValidationError"),
410 }
411 }
412
413 #[test]
414 fn test_pricing_error_creation() {
415 let error = OptionsError::pricing_error("black_scholes", "invalid parameters");
416 match error {
417 OptionsError::PricingError { method, reason } => {
418 assert_eq!(method, "black_scholes");
419 assert_eq!(reason, "invalid parameters");
420 }
421 _ => panic!("Expected PricingError"),
422 }
423 }
424
425 #[test]
426 fn test_greeks_error_creation() {
427 let error = OptionsError::greeks_error("delta", "calculation failed");
428 match error {
429 OptionsError::GreeksCalculationError { greek, reason } => {
430 assert_eq!(greek, "delta");
431 assert_eq!(reason, "calculation failed");
432 }
433 _ => panic!("Expected GreeksCalculationError"),
434 }
435 }
436
437 #[test]
438 fn test_time_error_creation() {
439 let error = OptionsError::time_error("expiry", "invalid date");
440 match error {
441 OptionsError::TimeError { operation, reason } => {
442 assert_eq!(operation, "expiry");
443 assert_eq!(reason, "invalid date");
444 }
445 _ => panic!("Expected TimeError"),
446 }
447 }
448
449 #[test]
450 fn test_payoff_error_creation() {
451 let error = OptionsError::payoff_error("invalid strike price");
452 match error {
453 OptionsError::PayoffError { reason } => {
454 assert_eq!(reason, "invalid strike price");
455 }
456 _ => panic!("Expected PayoffError"),
457 }
458 }
459
460 #[test]
461 fn test_update_error_creation() {
462 let error = OptionsError::update_error("volatility", "out of bounds");
463 match error {
464 OptionsError::UpdateError { field, reason } => {
465 assert_eq!(field, "volatility");
466 assert_eq!(reason, "out of bounds");
467 }
468 _ => panic!("Expected UpdateError"),
469 }
470 }
471
472 #[test]
473 fn test_error_display() {
474 let error = OptionsError::validation_error("price", "must be positive");
475 assert_eq!(
476 error.to_string(),
477 "Validation error for field 'price': must be positive"
478 );
479
480 let error = OptionsError::pricing_error("black_scholes", "invalid parameters");
481 assert_eq!(
482 error.to_string(),
483 "Pricing error using method 'black_scholes': invalid parameters"
484 );
485 }
486
487 #[test]
488 fn test_invalid_step_count_variant() {
489 let error = OptionsError::InvalidStepCount {
490 operation: "binomial",
491 };
492 assert_eq!(
493 format!("{error}"),
494 "invalid step count: binomial requires at least one step"
495 );
496 }
497
498 #[test]
499 fn test_implied_volatility_invariant_variant() {
500 let error = OptionsError::ImpliedVolatilityInvariant {
501 reason: "mid_vol must be non-negative".to_string(),
502 };
503 assert!(
504 format!("{error}")
505 .contains("implied volatility invariant breached: mid_vol must be non-negative")
506 );
507 }
508
509 #[test]
510 fn test_to_box_dyn_error_conversion() {
511 let error = OptionsError::validation_error("price", "must be positive");
512 let boxed: Box<dyn std::error::Error> = error.into();
513 assert_eq!(
514 boxed.to_string(),
515 "Validation error for field 'price': must be positive"
516 );
517 }
518
519 #[test]
520 fn test_error_is_send_and_sync() {
521 fn assert_send_sync<T: Send + Sync>() {}
522 assert_send_sync::<OptionsError>();
523 }
524
525 #[test]
526 fn test_options_result_type() {
527 let success: OptionsResult<i32> = Ok(42);
528 let failure: OptionsResult<i32> = Err(OptionsError::validation_error("test", "error"));
529
530 assert!(success.is_ok());
531 assert!(failure.is_err());
532 }
533}
534
535#[cfg(test)]
536mod tests_extended {
537 use super::*;
538
539 #[test]
540 fn test_error_chaining_via_display() {
541 let error1 = OptionsError::validation_error("strike", "invalid value");
542 let rendered = error1.to_string();
543 assert!(rendered.contains("invalid value"));
544 assert!(rendered.contains("strike"));
545 }
546
547 #[test]
548 fn test_complex_error_scenario() {
549 fn nested_function() -> OptionsResult<()> {
550 Err(OptionsError::validation_error("nested", "inner error"))
551 }
552
553 fn outer_function() -> OptionsResult<()> {
554 nested_function().map_err(|e| OptionsError::time_error("outer", &e.to_string()))
555 }
556
557 let result = outer_function();
558 assert!(result.is_err());
559 let error = result.unwrap_err();
560 assert!(matches!(error, OptionsError::TimeError { .. }));
561 }
562
563 #[test]
564 fn test_validation_combinations() {
565 let errors = vec![
566 OptionsError::validation_error("price", "negative value"),
567 OptionsError::validation_error("strike", "too high"),
568 OptionsError::validation_error("expiry", "past date"),
569 ];
570
571 for error in errors {
572 match error {
573 OptionsError::ValidationError { field, reason } => {
574 assert!(!field.is_empty());
575 assert!(!reason.is_empty());
576 }
577 _ => panic!("Expected ValidationError"),
578 }
579 }
580 }
581
582 #[test]
583 fn test_pricing_error_variants() {
584 let methods = ["black_scholes", "binomial", "monte_carlo"];
585 let reasons = ["invalid vol", "negative rate", "bad params"];
586
587 for (method, reason) in methods.iter().zip(reasons.iter()) {
588 let error = OptionsError::pricing_error(method, reason);
589 let error_str = error.to_string();
590 assert!(error_str.contains(method));
591 assert!(error_str.contains(reason));
592 }
593 }
594
595 #[test]
596 fn test_error_display_preserves_message() {
597 let original = "preserve this message";
598 let error = OptionsError::validation_error("field", original);
599 assert!(error.to_string().contains(original));
600 }
601
602 #[test]
603 fn test_option_result_operations() {
604 let success: OptionsResult<i32> = Ok(42);
605 let failure: OptionsResult<i32> = Err(OptionsError::validation_error("test", "error"));
606
607 let mapped_success = success.map(|x| x * 2);
608 let mapped_failure = failure.map(|x| x * 2);
609
610 assert_eq!(mapped_success.unwrap(), 84);
611 assert!(mapped_failure.is_err());
612 }
613
614 #[test]
615 fn test_nested_error_handling() {
616 fn process_value(value: i32) -> OptionsResult<i32> {
617 if value < 0 {
618 Err(OptionsError::validation_error("value", "must be positive"))
619 } else {
620 Ok(value)
621 }
622 }
623
624 let results: Vec<OptionsResult<i32>> =
625 vec![-1, 0, 1].into_iter().map(process_value).collect();
626
627 assert_eq!(results.len(), 3);
628 assert!(results[0].is_err());
629 assert!(results[1].is_ok());
630 assert!(results[2].is_ok());
631 }
632
633 #[test]
634 fn test_options_error_greeks_calculation_error() {
635 let error = OptionsError::GreeksCalculationError {
636 greek: "Delta".to_string(),
637 reason: "Division by zero".to_string(),
638 };
639 assert_eq!(
640 format!("{error}"),
641 "Error calculating greek 'Delta': Division by zero"
642 );
643 }
644
645 #[test]
646 fn test_options_error_time_error() {
647 let error = OptionsError::TimeError {
648 operation: "Option maturity".to_string(),
649 reason: "Negative time value".to_string(),
650 };
651 assert_eq!(
652 format!("{error}"),
653 "Time calculation error in 'Option maturity': Negative time value"
654 );
655 }
656
657 #[test]
658 fn test_options_error_payoff_error() {
659 let error = OptionsError::PayoffError {
660 reason: "Payoff cannot be negative".to_string(),
661 };
662 assert_eq!(
663 format!("{error}"),
664 "Payoff calculation error: Payoff cannot be negative"
665 );
666 }
667
668 #[test]
669 fn test_options_error_update_error() {
670 let error = OptionsError::UpdateError {
671 field: "Volatility".to_string(),
672 reason: "Invalid update value".to_string(),
673 };
674 assert_eq!(
675 format!("{error}"),
676 "Update error for field 'Volatility': Invalid update value"
677 );
678 }
679}