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}