Skip to main content

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}