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    /// The current time was asked for and this build has no clock.
170    #[error(transparent)]
171    Clock(#[from] crate::error::ClockError),
172}
173
174/// Specific errors that can occur in strategy operations
175///
176/// This enum captures detailed error types related to strategy operations,
177/// providing specific information about why a strategy operation failed.
178/// These errors typically occur during strategy management operations
179/// such as adding positions, updating configurations, or executing trades.
180///
181/// # Variants
182///
183/// * `UnsupportedOperation` - Indicates that the attempted operation is not
184///   supported by the particular strategy type.
185///
186/// * `StrategyFull` - Indicates that the strategy has reached its maximum
187///   capacity for positions.
188///
189/// * `InvalidConfiguration` - Indicates that the strategy configuration is
190///   invalid or inconsistent.
191///
192/// # Usage
193///
194/// Used when validating and executing strategy operations to provide detailed
195/// error information about why an operation could not be completed.
196#[derive(Error, Debug)]
197pub enum StrategyErrorKind {
198    /// Operation is not supported by this strategy
199    ///
200    /// Occurs when attempting to perform an operation that is not compatible
201    /// with the current strategy type.
202    #[error("Operation '{operation}' not supported by strategy '{strategy_type}'")]
203    UnsupportedOperation {
204        /// The type of strategy that doesn't support the operation
205        strategy_type: String,
206
207        /// The name of the operation that was attempted
208        operation: String,
209    },
210
211    /// Strategy has reached its maximum capacity
212    ///
213    /// Occurs when attempting to add more positions than a strategy can handle.
214    #[error("Strategy '{strategy_type}' is full (max positions: {max_positions})")]
215    StrategyFull {
216        /// The type of strategy that reached its capacity
217        strategy_type: String,
218
219        /// The maximum number of positions the strategy can hold
220        max_positions: usize,
221    },
222
223    /// Invalid strategy configuration
224    ///
225    /// Occurs when the strategy's configuration parameters are invalid or inconsistent.
226    #[error("Invalid strategy configuration: {0}")]
227    InvalidConfiguration(String),
228}
229
230/// Errors related to position validation
231///
232/// This enum represents errors that can occur during position validation checks.
233/// These errors typically happen when attempting to create or modify a position
234/// with invalid or incompatible parameters.
235///
236/// # Variants
237///
238/// * `InvalidSize` - The position size is invalid (e.g., zero, negative, or too large).
239///
240/// * `InvalidPrice` - The position price is invalid (e.g., negative or unrealistic).
241///
242/// * `IncompatibleSide` - The position side (long/short) is incompatible with the strategy.
243///
244/// * `IncompatibleStyle` - The option style is incompatible with the strategy.
245///
246/// * `InvalidPosition` - The position is invalid for other specific reasons.
247///
248///
249/// # Usage
250///
251/// Used when validating positions to ensure they meet all requirements before
252/// being added to a strategy or executed in a trading system.
253#[derive(Error, Debug)]
254pub enum PositionValidationErrorKind {
255    /// Position size is invalid
256    ///
257    /// Occurs when the specified position size violates size constraints.
258    #[error("Invalid position size {size}: {reason}")]
259    InvalidSize {
260        /// The invalid size value
261        size: f64,
262
263        /// Explanation of why the size is invalid
264        reason: String,
265    },
266
267    /// Position price is invalid
268    ///
269    /// Occurs when the specified price violates price constraints or is unrealistic.
270    #[error("Invalid position price {price}: {reason}")]
271    InvalidPrice {
272        /// The invalid price value
273        price: f64,
274
275        /// Explanation of why the price is invalid
276        reason: String,
277    },
278
279    /// Position type is incompatible with strategy
280    ///
281    /// Occurs when the position side (long/short) conflicts with strategy requirements.
282    #[error("Incompatible position side {position_side:?}: {reason}")]
283    IncompatibleSide {
284        /// The incompatible position side
285        position_side: Side,
286
287        /// Explanation of why the side is incompatible
288        reason: String,
289    },
290
291    /// Option style is incompatible with strategy
292    ///
293    /// Occurs when the option style (American/European) conflicts with strategy requirements.
294    #[error("Incompatible option style {style:?}: {reason}")]
295    IncompatibleStyle {
296        /// The incompatible option style
297        style: OptionStyle,
298
299        /// Explanation of why the style is incompatible
300        reason: String,
301    },
302
303    /// Position is invalid for other reasons
304    ///
305    /// A general error for positions that fail validation for reasons not covered by other variants.
306    #[error("Invalid position: {reason}")]
307    InvalidPosition {
308        /// Explanation of why the position is invalid
309        reason: String,
310    },
311}
312
313/// Represents errors related to position limits in trading operations.
314///
315/// This enum captures different types of limit violations that can occur when
316/// managing trading positions, such as exceeding the maximum number of allowed
317/// positions or reaching the maximum exposure threshold for a portfolio.
318///
319/// # Variants
320///
321/// * `MaxPositionsReached` - Error indicating that the maximum number of allowed
322///   positions has been reached, preventing the creation of additional positions.
323///
324/// * `MaxExposureReached` - Error indicating that the maximum allowed financial
325///   exposure has been reached, preventing positions that would increase exposure.
326///
327/// # Usage
328///
329/// This error type is typically used in position management systems, risk control
330/// mechanisms, and trading platforms to enforce safety limits and prevent excessive
331/// risk taking.
332///
333#[derive(Error, Debug)]
334pub enum PositionLimitErrorKind {
335    /// Error indicating the maximum number of positions has been reached
336    ///
337    /// This variant is triggered when attempting to create a new position would
338    /// exceed the configured maximum number of positions allowed in a portfolio
339    /// or trading account.
340    #[error("Maximum positions reached: {current}/{maximum}")]
341    MaxPositionsReached {
342        /// The current number of positions in the portfolio
343        current: usize,
344        /// The maximum number of positions allowed
345        maximum: usize,
346    },
347
348    /// Error indicating the maximum allowed exposure has been reached
349    ///
350    /// This variant is triggered when a new position or modification would cause
351    /// the total portfolio exposure to exceed the configured maximum risk threshold.
352    /// Exposure is typically measured in monetary terms based on position value or risk.
353    #[error("Maximum exposure reached: {current_exposure}/{max_exposure}")]
354    MaxExposureReached {
355        /// The current financial exposure level of the portfolio
356        current_exposure: f64,
357        /// The maximum allowed exposure level
358        max_exposure: f64,
359    },
360}
361
362/// Represents errors related to position update in trading operations.
363///
364/// This enum captures a specific error in an option position update.
365/// The specified field to be updated can return an error.
366///
367/// # Variants
368///
369/// * `PositionFieldUpdateFailure` - Error in the update of a specific positon field.
370///
371/// # Usage
372///
373/// This error type is typically used in position management systems and trading platforms to
374/// prevent a wrong position update.
375///
376#[derive(Error, Debug)]
377pub enum PositionUpdateErrorKind {
378    /// Error during position update
379    ///
380    /// This variant is triggered when attempts to update option position parameters or data fail.
381    #[error("Position update error for field '{field}': {reason}")]
382    PositionFieldUpdateFailure {
383        /// The position field that failed to update
384        field: String,
385        /// Detailed explanation of the position update failure reason
386        reason: String,
387    },
388}
389
390/// Factory methods for creating position-related errors
391///
392/// This implementation provides a set of convenience factory methods for creating
393/// different types of position errors. These methods create properly structured
394/// error instances with clear, descriptive information about what went wrong.
395///
396/// # Methods
397///
398/// These factory methods simplify error creation throughout the codebase and ensure
399/// that errors have consistent formatting and information.
400impl PositionError {
401    /// Creates an error for operations not supported by a specific strategy type
402    ///
403    /// # Parameters
404    ///
405    /// * `strategy_type` - The name or identifier of the strategy that doesn't support the operation
406    /// * `operation` - The name of the unsupported operation that was attempted
407    ///
408    /// # Returns
409    ///
410    /// A `PositionError::StrategyError` variant with UnsupportedOperation details
411    #[must_use]
412    #[cold]
413    #[inline(never)]
414    pub fn unsupported_operation(strategy_type: &str, operation: &str) -> Self {
415        PositionError::StrategyError(StrategyErrorKind::UnsupportedOperation {
416            strategy_type: strategy_type.to_string(),
417            operation: operation.to_string(),
418        })
419    }
420
421    /// Creates an error when a strategy has reached its maximum position capacity
422    ///
423    /// # Parameters
424    ///
425    /// * `strategy_type` - The name or identifier of the strategy that is at capacity
426    /// * `max_positions` - The maximum number of positions the strategy can hold
427    ///
428    /// # Returns
429    ///
430    /// A `PositionError::StrategyError` variant with StrategyFull details
431    #[must_use]
432    #[cold]
433    #[inline(never)]
434    pub fn strategy_full(strategy_type: &str, max_positions: usize) -> Self {
435        PositionError::StrategyError(StrategyErrorKind::StrategyFull {
436            strategy_type: strategy_type.to_string(),
437            max_positions,
438        })
439    }
440
441    /// Creates an error for invalid position size values
442    ///
443    /// # Parameters
444    ///
445    /// * `size` - The invalid position size value
446    /// * `reason` - A description of why the size is invalid
447    ///
448    /// # Returns
449    ///
450    /// A `PositionError::ValidationError` variant with InvalidSize details
451    #[must_use]
452    #[cold]
453    #[inline(never)]
454    pub fn invalid_position_size(size: f64, reason: &str) -> Self {
455        PositionError::ValidationError(PositionValidationErrorKind::InvalidSize {
456            size,
457            reason: reason.to_string(),
458        })
459    }
460
461    /// Creates an error for incompatible position side/direction
462    ///
463    /// # Parameters
464    ///
465    /// * `position_side` - The position side (Long or Short) that is incompatible
466    /// * `reason` - A description of why the position side is incompatible
467    ///
468    /// # Returns
469    ///
470    /// A `PositionError::ValidationError` variant with IncompatibleSide details
471    #[must_use]
472    #[cold]
473    #[inline(never)]
474    pub fn invalid_position_type(position_side: Side, reason: String) -> Self {
475        PositionError::ValidationError(PositionValidationErrorKind::IncompatibleSide {
476            position_side,
477            reason,
478        })
479    }
480
481    /// Creates an error for incompatible option style
482    ///
483    /// # Parameters
484    ///
485    /// * `style` - The option style (Call or Put) that is incompatible
486    /// * `reason` - A description of why the option style is incompatible
487    ///
488    /// # Returns
489    ///
490    /// A `PositionError::ValidationError` variant with IncompatibleStyle details
491    #[must_use]
492    #[cold]
493    #[inline(never)]
494    pub fn invalid_position_style(style: OptionStyle, reason: String) -> Self {
495        PositionError::ValidationError(PositionValidationErrorKind::IncompatibleStyle {
496            style,
497            reason,
498        })
499    }
500
501    /// Creates a generic invalid position error
502    ///
503    /// # Parameters
504    ///
505    /// * `reason` - A description of why the position is invalid
506    ///
507    /// # Returns
508    ///
509    /// A `PositionError::ValidationError` variant with InvalidPosition details
510    #[must_use]
511    #[cold]
512    #[inline(never)]
513    pub fn invalid_position(reason: &str) -> Self {
514        PositionError::ValidationError(PositionValidationErrorKind::InvalidPosition {
515            reason: reason.to_string(),
516        })
517    }
518
519    /// Creates an error for invalid position update
520    ///
521    /// # Parameters
522    ///
523    /// * `field` - The option position field that failed to update
524    /// * `reason` - A description of why the option position field failed to update
525    ///
526    /// # Returns
527    ///
528    /// A `PositionError::UpdateError` variant with PositionFieldUpdateFailure details
529    #[must_use]
530    #[cold]
531    #[inline(never)]
532    pub fn invalid_position_update(field: String, reason: String) -> Self {
533        PositionError::UpdateError(PositionUpdateErrorKind::PositionFieldUpdateFailure {
534            field,
535            reason,
536        })
537    }
538}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543
544    #[test]
545    fn test_error_messages() {
546        let error = PositionError::unsupported_operation("TestStrategy", "add_position");
547        assert!(error.to_string().contains("TestStrategy"));
548        assert!(error.to_string().contains("add_position"));
549    }
550
551    #[test]
552    fn test_invalid_position_size() {
553        let error = PositionError::invalid_position_size(-1.0, "Size cannot be negative");
554        assert!(matches!(
555            error,
556            PositionError::ValidationError(PositionValidationErrorKind::InvalidSize { .. })
557        ));
558    }
559}
560
561#[cfg(test)]
562mod tests_extended {
563    use super::*;
564
565    #[test]
566    fn test_validation_error_display() {
567        let error = PositionValidationErrorKind::InvalidSize {
568            size: -1.0,
569            reason: "Size must be positive".to_string(),
570        };
571
572        assert!(error.to_string().contains("-1"));
573        assert!(error.to_string().contains("Size must be positive"));
574
575        let error = PositionValidationErrorKind::IncompatibleSide {
576            position_side: Side::Long,
577            reason: "Strategy requires short positions".to_string(),
578        };
579        assert!(error.to_string().contains("Long"));
580        assert!(error.to_string().contains("Strategy requires short"));
581    }
582
583    #[test]
584    fn test_limit_error_display() {
585        let error = PositionLimitErrorKind::MaxPositionsReached {
586            current: 5,
587            maximum: 4,
588        };
589        assert!(error.to_string().contains("5"));
590        assert!(error.to_string().contains("4"));
591
592        let error = PositionLimitErrorKind::MaxExposureReached {
593            current_exposure: 1000.0,
594            max_exposure: 500.0,
595        };
596        assert!(error.to_string().contains("1000"));
597        assert!(error.to_string().contains("500"));
598    }
599
600    #[test]
601    fn test_invalid_position_constructor() {
602        let error = PositionError::invalid_position("bad position");
603        assert!(matches!(
604            error,
605            PositionError::ValidationError(PositionValidationErrorKind::InvalidPosition { .. })
606        ));
607    }
608
609    #[test]
610    fn test_position_error_helper_methods() {
611        let error = PositionError::invalid_position_size(-1.0, "Must be positive");
612        assert!(matches!(
613            error,
614            PositionError::ValidationError(PositionValidationErrorKind::InvalidSize { .. })
615        ));
616
617        let error = PositionError::invalid_position_type(
618            Side::Long,
619            "Strategy requires short positions".to_string(),
620        );
621        assert!(matches!(
622            error,
623            PositionError::ValidationError(PositionValidationErrorKind::IncompatibleSide { .. })
624        ));
625    }
626
627    #[test]
628    fn test_strategy_error_helper_methods() {
629        let error = PositionError::strategy_full("Iron Condor", 4);
630        assert!(matches!(
631            error,
632            PositionError::StrategyError(StrategyErrorKind::StrategyFull { .. })
633        ));
634
635        let error = PositionError::unsupported_operation("Iron Condor", "add_leg");
636        assert!(matches!(
637            error,
638            PositionError::StrategyError(StrategyErrorKind::UnsupportedOperation { .. })
639        ));
640    }
641
642    #[test]
643    fn test_position_error_validation_error() {
644        let error = PositionError::ValidationError(PositionValidationErrorKind::InvalidSize {
645            size: -1.0,
646            reason: "Size must be positive".to_string(),
647        });
648        assert_eq!(
649            format!("{error}"),
650            "Validation error: Invalid position size -1: Size must be positive"
651        );
652    }
653
654    #[test]
655    fn test_position_error_limit_error() {
656        let error = PositionError::LimitError(PositionLimitErrorKind::MaxPositionsReached {
657            current: 10,
658            maximum: 5,
659        });
660        assert_eq!(
661            format!("{error}"),
662            "Limit error: Maximum positions reached: 10/5"
663        );
664    }
665
666    #[test]
667    fn test_strategy_error_strategy_full() {
668        let error = StrategyErrorKind::StrategyFull {
669            strategy_type: "Iron Condor".to_string(),
670            max_positions: 10,
671        };
672        assert_eq!(
673            format!("{error}"),
674            "Strategy 'Iron Condor' is full (max positions: 10)"
675        );
676    }
677
678    #[test]
679    fn test_strategy_error_invalid_configuration() {
680        let error = StrategyErrorKind::InvalidConfiguration("Invalid risk parameters".to_string());
681        assert_eq!(
682            format!("{error}"),
683            "Invalid strategy configuration: Invalid risk parameters"
684        );
685    }
686
687    #[test]
688    fn test_position_validation_error_invalid_price() {
689        let error = PositionValidationErrorKind::InvalidPrice {
690            price: 105.5,
691            reason: "Outside allowable range".to_string(),
692        };
693        assert_eq!(
694            format!("{error}"),
695            "Invalid position price 105.5: Outside allowable range"
696        );
697    }
698
699    #[test]
700    fn test_position_validation_error_invalid_position() {
701        let error = PositionValidationErrorKind::InvalidPosition {
702            reason: "Position size exceeds margin".to_string(),
703        };
704        assert_eq!(
705            format!("{error}"),
706            "Invalid position: Position size exceeds margin"
707        );
708    }
709
710    #[test]
711    fn test_position_validation_error_incompatible_style() {
712        let error = PositionValidationErrorKind::IncompatibleStyle {
713            style: OptionStyle::Call,
714            reason: "Unsupported for Call options".to_string(),
715        };
716        assert_eq!(
717            format!("{error}"),
718            "Incompatible option style OptionStyle::Call: Unsupported for Call options"
719        );
720    }
721
722    #[test]
723    fn test_position_update_error() {
724        let error = PositionUpdateErrorKind::PositionFieldUpdateFailure {
725            field: "premium".to_string(),
726            reason: "Missing call ask price for long call position".to_string(),
727        };
728        assert_eq!(
729            format!("{error}"),
730            "Position update error for field 'premium': Missing call ask price for long call position"
731        );
732    }
733
734    #[test]
735    fn test_position_update_error_helper_method() {
736        let error = PositionError::invalid_position_update(
737            "premium".to_string(),
738            "Missing call ask price for long call position".to_string(),
739        );
740        assert_eq!(
741            format!("{error}"),
742            "Update error: Position update error for field 'premium': Missing call ask price for long call position"
743        );
744    }
745}