Skip to main content

optionstratlib_core/error/
position.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 20/12/24
5******************************************************************************/
6
7//! # Position Error Module
8//!
9//! This module provides error handling for position-related operations in option trading strategies.
10//! It defines error types to handle various scenarios related to position validation, strategy
11//! operations, and position limits.
12//!
13//! ## Error Types
14//!
15//! ### Position Error (`PositionError`)
16//! The main error type with six variants:
17//! * `StrategyError` - For strategy operation failures
18//! * `ValidationError` - For position validation failures
19//! * `LimitError` - For position limit violations
20//! * `UpdateError` - For position update failures
21//! * `DecimalError` - For `Decimal` arithmetic failures
22//! * `PositiveError` - For `Positive` arithmetic failures
23//!
24//! ### Strategy Errors (`StrategyErrorKind`)
25//! Handles specific strategy-related errors:
26//! * Unsupported operations
27//! * Strategy capacity limits
28//! * Invalid configurations
29//!
30//! ### Validation Errors (`PositionValidationErrorKind`)
31//! Handles position validation failures:
32//! * Invalid position sizes
33//! * Invalid prices
34//! * Incompatible sides (Long/Short)
35//! * Incompatible styles (Call/Put)
36//! * General position invalidity
37//!
38//! ### Limit Errors (`PositionLimitErrorKind`)
39//! Handles position limit violations:
40//! * Maximum positions reached
41//! * Maximum exposure reached
42//!
43//! ## Usage Example
44//!
45//! ```rust
46//! use optionstratlib_core::error::position::PositionError;
47//!
48//! fn validate_position_size(size: f64) -> Result<(), PositionError> {
49//!     if size <= 0.0 {
50//!         return Err(PositionError::invalid_position_size(
51//!             size,
52//!             "Position size must be positive"
53//!         ));
54//!     }
55//!     Ok(())
56//! }
57//! ```
58//!
59//! ## Helper Methods
60//!
61//! The module provides several helper methods for creating common errors:
62//! * `unsupported_operation` - Creates an error for unsupported strategy operations
63//! * `strategy_full` - Creates an error when strategy capacity is reached
64//! * `invalid_position_size` - Creates an error for invalid position sizes
65//! * `invalid_position_type` - Creates an error for incompatible position sides
66//! * `invalid_position_style` - Creates an error for incompatible option styles
67//! * `invalid_position` - Creates a general position validation error
68//!
69//! All error types implement `std::error::Error` and `std::fmt::Display` for proper
70//! error handling and formatting capabilities.
71//!
72//! Target crate (ADR-0001 D6, roadmap M1-14): **core**. Owns `PositionError`, `StrategyErrorKind` (a kind enum, no strategy type inside).
73//!
74//! The `...Kind` enums are canonical in this module (`optionstratlib_core::error::position`) and are not
75//! flattened into `optionstratlib_core::error`: the facade gathers every crate's errors
76//! in one `optionstratlib::error` module, where flat kind names would collide
77//! (`StrategyErrorKind` is defined in both core `error::position` and market
78//! `error::chains`, `PriceErrorKind` in both analytics `error::probability`
79//! and strategies `error::strategies`), so each crate keeps its kinds in its
80//! kind module and the facade re-exports that module (#550).
81
82use crate::model::types::{OptionStyle, Side};
83use thiserror::Error;
84
85/// Represents errors that can occur when managing positions in strategies
86///
87/// This enum provides a top-level categorization of position-related errors,
88/// grouping them by their source or nature. It helps with routing errors to
89/// appropriate handlers and providing context-aware error messages.
90///
91/// # Variants
92///
93/// * `StrategyError` - Errors related to strategy operations such as configuration
94///   issues or capacity limitations.
95///
96/// * `ValidationError` - Errors related to position validation including issues
97///   with size, price, or compatibility with strategy requirements.
98///
99/// * `LimitError` - Errors related to position limits such as maximum number of
100///   positions or maximum exposure thresholds.
101///
102/// # Usage
103///
104/// This error type is typically used in trading systems where positions need to
105/// be validated, managed, and executed within the context of trading strategies.
106#[derive(Error, Debug)]
107pub enum PositionError {
108    /// Errors related to strategy operations
109    #[error("Strategy error: {0}")]
110    StrategyError(StrategyErrorKind),
111
112    /// Errors related to position validation
113    #[error("Validation error: {0}")]
114    ValidationError(PositionValidationErrorKind),
115
116    /// Errors related to position limits
117    #[error("Limit error: {0}")]
118    LimitError(PositionLimitErrorKind),
119
120    /// Error during position update
121    ///
122    /// This variant is triggered when attempts to update option position parameters or data fail.
123    // #[error("Position update error for field '{field}': {reason}")]
124    // UpdateError {
125    //     /// The position field that failed to update
126    //     field: String,
127    //     /// Detailed explanation of the position update failure
128    //     reason: String,
129    // },
130    /// Errors related to position update
131    #[error("Update error: {0}")]
132    UpdateError(PositionUpdateErrorKind),
133
134    /// Decimal arithmetic failures propagated from monetary-flow
135    /// computations (overflow, division by zero, conversion).
136    ///
137    /// Produced when a checked Decimal helper in `model::decimal`
138    /// surfaces a [`crate::error::DecimalError`] from inside a
139    /// `Position` method (for example `pnl_at_expiration` or
140    /// `unrealized_pnl`). Propagated transparently via the
141    /// `#[from]` cascade so callers keep matching `PositionError`.
142    #[error(transparent)]
143    DecimalError(#[from] crate::error::DecimalError),
144
145    /// `Positive` arithmetic failures propagated from cost and fee
146    /// computations (overflow, division by zero, broken positivity
147    /// invariant).
148    ///
149    /// Produced when a checked `Positive` helper surfaces a
150    /// [`positive::PositiveError`] from inside a `Position` method (for
151    /// example `total_cost` or `fees`, where the premium and the fees are
152    /// accumulated and then scaled by the contract quantity). Propagated
153    /// transparently via the `#[from]` cascade so callers keep matching
154    /// `PositionError`.
155    #[error(transparent)]
156    PositiveError(#[from] positive::PositiveError),
157
158    /// Contract-level failures propagated from the option the position
159    /// holds.
160    ///
161    /// Produced when `Options::intrinsic_value` surfaces an
162    /// [`crate::error::OptionsError`] inside a `Position` or `Leg` P&L
163    /// method. Both types are core-owned, so the position layer reports a
164    /// contract failure with its own error instead of borrowing the pricing
165    /// layer's (roadmap M1-10).
166    #[error(transparent)]
167    Options(#[from] crate::error::OptionsError),
168}
169
170/// Specific errors that can occur in strategy operations
171///
172/// This enum captures detailed error types related to strategy operations,
173/// providing specific information about why a strategy operation failed.
174/// These errors typically occur during strategy management operations
175/// such as adding positions, updating configurations, or executing trades.
176///
177/// # Variants
178///
179/// * `UnsupportedOperation` - Indicates that the attempted operation is not
180///   supported by the particular strategy type.
181///
182/// * `StrategyFull` - Indicates that the strategy has reached its maximum
183///   capacity for positions.
184///
185/// * `InvalidConfiguration` - Indicates that the strategy configuration is
186///   invalid or inconsistent.
187///
188/// # Usage
189///
190/// Used when validating and executing strategy operations to provide detailed
191/// error information about why an operation could not be completed.
192#[derive(Error, Debug)]
193pub enum StrategyErrorKind {
194    /// Operation is not supported by this strategy
195    ///
196    /// Occurs when attempting to perform an operation that is not compatible
197    /// with the current strategy type.
198    #[error("Operation '{operation}' not supported by strategy '{strategy_type}'")]
199    UnsupportedOperation {
200        /// The type of strategy that doesn't support the operation
201        strategy_type: String,
202
203        /// The name of the operation that was attempted
204        operation: String,
205    },
206
207    /// Strategy has reached its maximum capacity
208    ///
209    /// Occurs when attempting to add more positions than a strategy can handle.
210    #[error("Strategy '{strategy_type}' is full (max positions: {max_positions})")]
211    StrategyFull {
212        /// The type of strategy that reached its capacity
213        strategy_type: String,
214
215        /// The maximum number of positions the strategy can hold
216        max_positions: usize,
217    },
218
219    /// Invalid strategy configuration
220    ///
221    /// Occurs when the strategy's configuration parameters are invalid or inconsistent.
222    #[error("Invalid strategy configuration: {0}")]
223    InvalidConfiguration(String),
224}
225
226/// Errors related to position validation
227///
228/// This enum represents errors that can occur during position validation checks.
229/// These errors typically happen when attempting to create or modify a position
230/// with invalid or incompatible parameters.
231///
232/// # Variants
233///
234/// * `InvalidSize` - The position size is invalid (e.g., zero, negative, or too large).
235///
236/// * `InvalidPrice` - The position price is invalid (e.g., negative or unrealistic).
237///
238/// * `IncompatibleSide` - The position side (long/short) is incompatible with the strategy.
239///
240/// * `IncompatibleStyle` - The option style is incompatible with the strategy.
241///
242/// * `InvalidPosition` - The position is invalid for other specific reasons.
243///
244///
245/// # Usage
246///
247/// Used when validating positions to ensure they meet all requirements before
248/// being added to a strategy or executed in a trading system.
249#[derive(Error, Debug)]
250pub enum PositionValidationErrorKind {
251    /// Position size is invalid
252    ///
253    /// Occurs when the specified position size violates size constraints.
254    #[error("Invalid position size {size}: {reason}")]
255    InvalidSize {
256        /// The invalid size value
257        size: f64,
258
259        /// Explanation of why the size is invalid
260        reason: String,
261    },
262
263    /// Position price is invalid
264    ///
265    /// Occurs when the specified price violates price constraints or is unrealistic.
266    #[error("Invalid position price {price}: {reason}")]
267    InvalidPrice {
268        /// The invalid price value
269        price: f64,
270
271        /// Explanation of why the price is invalid
272        reason: String,
273    },
274
275    /// Position type is incompatible with strategy
276    ///
277    /// Occurs when the position side (long/short) conflicts with strategy requirements.
278    #[error("Incompatible position side {position_side:?}: {reason}")]
279    IncompatibleSide {
280        /// The incompatible position side
281        position_side: Side,
282
283        /// Explanation of why the side is incompatible
284        reason: String,
285    },
286
287    /// Option style is incompatible with strategy
288    ///
289    /// Occurs when the option style (American/European) conflicts with strategy requirements.
290    #[error("Incompatible option style {style:?}: {reason}")]
291    IncompatibleStyle {
292        /// The incompatible option style
293        style: OptionStyle,
294
295        /// Explanation of why the style is incompatible
296        reason: String,
297    },
298
299    /// Position is invalid for other reasons
300    ///
301    /// A general error for positions that fail validation for reasons not covered by other variants.
302    #[error("Invalid position: {reason}")]
303    InvalidPosition {
304        /// Explanation of why the position is invalid
305        reason: String,
306    },
307}
308
309/// Represents errors related to position limits in trading operations.
310///
311/// This enum captures different types of limit violations that can occur when
312/// managing trading positions, such as exceeding the maximum number of allowed
313/// positions or reaching the maximum exposure threshold for a portfolio.
314///
315/// # Variants
316///
317/// * `MaxPositionsReached` - Error indicating that the maximum number of allowed
318///   positions has been reached, preventing the creation of additional positions.
319///
320/// * `MaxExposureReached` - Error indicating that the maximum allowed financial
321///   exposure has been reached, preventing positions that would increase exposure.
322///
323/// # Usage
324///
325/// This error type is typically used in position management systems, risk control
326/// mechanisms, and trading platforms to enforce safety limits and prevent excessive
327/// risk taking.
328///
329#[derive(Error, Debug)]
330pub enum PositionLimitErrorKind {
331    /// Error indicating the maximum number of positions has been reached
332    ///
333    /// This variant is triggered when attempting to create a new position would
334    /// exceed the configured maximum number of positions allowed in a portfolio
335    /// or trading account.
336    #[error("Maximum positions reached: {current}/{maximum}")]
337    MaxPositionsReached {
338        /// The current number of positions in the portfolio
339        current: usize,
340        /// The maximum number of positions allowed
341        maximum: usize,
342    },
343
344    /// Error indicating the maximum allowed exposure has been reached
345    ///
346    /// This variant is triggered when a new position or modification would cause
347    /// the total portfolio exposure to exceed the configured maximum risk threshold.
348    /// Exposure is typically measured in monetary terms based on position value or risk.
349    #[error("Maximum exposure reached: {current_exposure}/{max_exposure}")]
350    MaxExposureReached {
351        /// The current financial exposure level of the portfolio
352        current_exposure: f64,
353        /// The maximum allowed exposure level
354        max_exposure: f64,
355    },
356}
357
358/// Represents errors related to position update in trading operations.
359///
360/// This enum captures a specific error in an option position update.
361/// The specified field to be updated can return an error.
362///
363/// # Variants
364///
365/// * `PositionFieldUpdateFailure` - Error in the update of a specific positon field.
366///
367/// # Usage
368///
369/// This error type is typically used in position management systems and trading platforms to
370/// prevent a wrong position update.
371///
372#[derive(Error, Debug)]
373pub enum PositionUpdateErrorKind {
374    /// Error during position update
375    ///
376    /// This variant is triggered when attempts to update option position parameters or data fail.
377    #[error("Position update error for field '{field}': {reason}")]
378    PositionFieldUpdateFailure {
379        /// The position field that failed to update
380        field: String,
381        /// Detailed explanation of the position update failure reason
382        reason: String,
383    },
384}
385
386/// Factory methods for creating position-related errors
387///
388/// This implementation provides a set of convenience factory methods for creating
389/// different types of position errors. These methods create properly structured
390/// error instances with clear, descriptive information about what went wrong.
391///
392/// # Methods
393///
394/// These factory methods simplify error creation throughout the codebase and ensure
395/// that errors have consistent formatting and information.
396impl PositionError {
397    /// Creates an error for operations not supported by a specific strategy type
398    ///
399    /// # Parameters
400    ///
401    /// * `strategy_type` - The name or identifier of the strategy that doesn't support the operation
402    /// * `operation` - The name of the unsupported operation that was attempted
403    ///
404    /// # Returns
405    ///
406    /// A `PositionError::StrategyError` variant with UnsupportedOperation details
407    #[must_use]
408    #[cold]
409    #[inline(never)]
410    pub fn unsupported_operation(strategy_type: &str, operation: &str) -> Self {
411        PositionError::StrategyError(StrategyErrorKind::UnsupportedOperation {
412            strategy_type: strategy_type.to_string(),
413            operation: operation.to_string(),
414        })
415    }
416
417    /// Creates an error when a strategy has reached its maximum position capacity
418    ///
419    /// # Parameters
420    ///
421    /// * `strategy_type` - The name or identifier of the strategy that is at capacity
422    /// * `max_positions` - The maximum number of positions the strategy can hold
423    ///
424    /// # Returns
425    ///
426    /// A `PositionError::StrategyError` variant with StrategyFull details
427    #[must_use]
428    #[cold]
429    #[inline(never)]
430    pub fn strategy_full(strategy_type: &str, max_positions: usize) -> Self {
431        PositionError::StrategyError(StrategyErrorKind::StrategyFull {
432            strategy_type: strategy_type.to_string(),
433            max_positions,
434        })
435    }
436
437    /// Creates an error for invalid position size values
438    ///
439    /// # Parameters
440    ///
441    /// * `size` - The invalid position size value
442    /// * `reason` - A description of why the size is invalid
443    ///
444    /// # Returns
445    ///
446    /// A `PositionError::ValidationError` variant with InvalidSize details
447    #[must_use]
448    #[cold]
449    #[inline(never)]
450    pub fn invalid_position_size(size: f64, reason: &str) -> Self {
451        PositionError::ValidationError(PositionValidationErrorKind::InvalidSize {
452            size,
453            reason: reason.to_string(),
454        })
455    }
456
457    /// Creates an error for incompatible position side/direction
458    ///
459    /// # Parameters
460    ///
461    /// * `position_side` - The position side (Long or Short) that is incompatible
462    /// * `reason` - A description of why the position side is incompatible
463    ///
464    /// # Returns
465    ///
466    /// A `PositionError::ValidationError` variant with IncompatibleSide details
467    #[must_use]
468    #[cold]
469    #[inline(never)]
470    pub fn invalid_position_type(position_side: Side, reason: String) -> Self {
471        PositionError::ValidationError(PositionValidationErrorKind::IncompatibleSide {
472            position_side,
473            reason,
474        })
475    }
476
477    /// Creates an error for incompatible option style
478    ///
479    /// # Parameters
480    ///
481    /// * `style` - The option style (Call or Put) that is incompatible
482    /// * `reason` - A description of why the option style is incompatible
483    ///
484    /// # Returns
485    ///
486    /// A `PositionError::ValidationError` variant with IncompatibleStyle details
487    #[must_use]
488    #[cold]
489    #[inline(never)]
490    pub fn invalid_position_style(style: OptionStyle, reason: String) -> Self {
491        PositionError::ValidationError(PositionValidationErrorKind::IncompatibleStyle {
492            style,
493            reason,
494        })
495    }
496
497    /// Creates a generic invalid position error
498    ///
499    /// # Parameters
500    ///
501    /// * `reason` - A description of why the position is invalid
502    ///
503    /// # Returns
504    ///
505    /// A `PositionError::ValidationError` variant with InvalidPosition details
506    #[must_use]
507    #[cold]
508    #[inline(never)]
509    pub fn invalid_position(reason: &str) -> Self {
510        PositionError::ValidationError(PositionValidationErrorKind::InvalidPosition {
511            reason: reason.to_string(),
512        })
513    }
514
515    /// Creates an error for invalid position update
516    ///
517    /// # Parameters
518    ///
519    /// * `field` - The option position field that failed to update
520    /// * `reason` - A description of why the option position field failed to update
521    ///
522    /// # Returns
523    ///
524    /// A `PositionError::UpdateError` variant with PositionFieldUpdateFailure details
525    #[must_use]
526    #[cold]
527    #[inline(never)]
528    pub fn invalid_position_update(field: String, reason: String) -> Self {
529        PositionError::UpdateError(PositionUpdateErrorKind::PositionFieldUpdateFailure {
530            field,
531            reason,
532        })
533    }
534}
535
536#[cfg(test)]
537mod tests {
538    use super::*;
539
540    #[test]
541    fn test_error_messages() {
542        let error = PositionError::unsupported_operation("TestStrategy", "add_position");
543        assert!(error.to_string().contains("TestStrategy"));
544        assert!(error.to_string().contains("add_position"));
545    }
546
547    #[test]
548    fn test_invalid_position_size() {
549        let error = PositionError::invalid_position_size(-1.0, "Size cannot be negative");
550        assert!(matches!(
551            error,
552            PositionError::ValidationError(PositionValidationErrorKind::InvalidSize { .. })
553        ));
554    }
555}
556
557#[cfg(test)]
558mod tests_extended {
559    use super::*;
560
561    #[test]
562    fn test_validation_error_display() {
563        let error = PositionValidationErrorKind::InvalidSize {
564            size: -1.0,
565            reason: "Size must be positive".to_string(),
566        };
567
568        assert!(error.to_string().contains("-1"));
569        assert!(error.to_string().contains("Size must be positive"));
570
571        let error = PositionValidationErrorKind::IncompatibleSide {
572            position_side: Side::Long,
573            reason: "Strategy requires short positions".to_string(),
574        };
575        assert!(error.to_string().contains("Long"));
576        assert!(error.to_string().contains("Strategy requires short"));
577    }
578
579    #[test]
580    fn test_limit_error_display() {
581        let error = PositionLimitErrorKind::MaxPositionsReached {
582            current: 5,
583            maximum: 4,
584        };
585        assert!(error.to_string().contains("5"));
586        assert!(error.to_string().contains("4"));
587
588        let error = PositionLimitErrorKind::MaxExposureReached {
589            current_exposure: 1000.0,
590            max_exposure: 500.0,
591        };
592        assert!(error.to_string().contains("1000"));
593        assert!(error.to_string().contains("500"));
594    }
595
596    #[test]
597    fn test_invalid_position_constructor() {
598        let error = PositionError::invalid_position("bad position");
599        assert!(matches!(
600            error,
601            PositionError::ValidationError(PositionValidationErrorKind::InvalidPosition { .. })
602        ));
603    }
604
605    #[test]
606    fn test_position_error_helper_methods() {
607        let error = PositionError::invalid_position_size(-1.0, "Must be positive");
608        assert!(matches!(
609            error,
610            PositionError::ValidationError(PositionValidationErrorKind::InvalidSize { .. })
611        ));
612
613        let error = PositionError::invalid_position_type(
614            Side::Long,
615            "Strategy requires short positions".to_string(),
616        );
617        assert!(matches!(
618            error,
619            PositionError::ValidationError(PositionValidationErrorKind::IncompatibleSide { .. })
620        ));
621    }
622
623    #[test]
624    fn test_strategy_error_helper_methods() {
625        let error = PositionError::strategy_full("Iron Condor", 4);
626        assert!(matches!(
627            error,
628            PositionError::StrategyError(StrategyErrorKind::StrategyFull { .. })
629        ));
630
631        let error = PositionError::unsupported_operation("Iron Condor", "add_leg");
632        assert!(matches!(
633            error,
634            PositionError::StrategyError(StrategyErrorKind::UnsupportedOperation { .. })
635        ));
636    }
637
638    #[test]
639    fn test_position_error_validation_error() {
640        let error = PositionError::ValidationError(PositionValidationErrorKind::InvalidSize {
641            size: -1.0,
642            reason: "Size must be positive".to_string(),
643        });
644        assert_eq!(
645            format!("{error}"),
646            "Validation error: Invalid position size -1: Size must be positive"
647        );
648    }
649
650    #[test]
651    fn test_position_error_limit_error() {
652        let error = PositionError::LimitError(PositionLimitErrorKind::MaxPositionsReached {
653            current: 10,
654            maximum: 5,
655        });
656        assert_eq!(
657            format!("{error}"),
658            "Limit error: Maximum positions reached: 10/5"
659        );
660    }
661
662    #[test]
663    fn test_strategy_error_strategy_full() {
664        let error = StrategyErrorKind::StrategyFull {
665            strategy_type: "Iron Condor".to_string(),
666            max_positions: 10,
667        };
668        assert_eq!(
669            format!("{error}"),
670            "Strategy 'Iron Condor' is full (max positions: 10)"
671        );
672    }
673
674    #[test]
675    fn test_strategy_error_invalid_configuration() {
676        let error = StrategyErrorKind::InvalidConfiguration("Invalid risk parameters".to_string());
677        assert_eq!(
678            format!("{error}"),
679            "Invalid strategy configuration: Invalid risk parameters"
680        );
681    }
682
683    #[test]
684    fn test_position_validation_error_invalid_price() {
685        let error = PositionValidationErrorKind::InvalidPrice {
686            price: 105.5,
687            reason: "Outside allowable range".to_string(),
688        };
689        assert_eq!(
690            format!("{error}"),
691            "Invalid position price 105.5: Outside allowable range"
692        );
693    }
694
695    #[test]
696    fn test_position_validation_error_invalid_position() {
697        let error = PositionValidationErrorKind::InvalidPosition {
698            reason: "Position size exceeds margin".to_string(),
699        };
700        assert_eq!(
701            format!("{error}"),
702            "Invalid position: Position size exceeds margin"
703        );
704    }
705
706    #[test]
707    fn test_position_validation_error_incompatible_style() {
708        let error = PositionValidationErrorKind::IncompatibleStyle {
709            style: OptionStyle::Call,
710            reason: "Unsupported for Call options".to_string(),
711        };
712        assert_eq!(
713            format!("{error}"),
714            "Incompatible option style OptionStyle::Call: Unsupported for Call options"
715        );
716    }
717
718    #[test]
719    fn test_position_update_error() {
720        let error = PositionUpdateErrorKind::PositionFieldUpdateFailure {
721            field: "premium".to_string(),
722            reason: "Missing call ask price for long call position".to_string(),
723        };
724        assert_eq!(
725            format!("{error}"),
726            "Position update error for field 'premium': Missing call ask price for long call position"
727        );
728    }
729
730    #[test]
731    fn test_position_update_error_helper_method() {
732        let error = PositionError::invalid_position_update(
733            "premium".to_string(),
734            "Missing call ask price for long call position".to_string(),
735        );
736        assert_eq!(
737            format!("{error}"),
738            "Update error: Position update error for field 'premium': Missing call ask price for long call position"
739        );
740    }
741}