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}