Skip to main content

optionstratlib_core/error/
settlement.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 5/11/26
5******************************************************************************/
6
7//! Target crate (ADR-0001 D6): **core**. Owns `SettlementError`.
8
9use crate::model::types::{OptionType, Side};
10use crate::model::{SettlementEvent, SettlementType};
11use chrono::{DateTime, Utc};
12use thiserror::Error;
13
14/// Reasons why [`settlement_obligation`](crate::model::settlement_obligation)
15/// cannot compute what an option position owes or receives.
16#[derive(Error, Debug, Clone, PartialEq, Eq)]
17#[non_exhaustive]
18pub enum SettlementError {
19    /// The contract's option type has no settlement rule. Only European and
20    /// American options are supported.
21    #[error(
22        "unsupported option type for settlement: {option_type} (supported: European, American)"
23    )]
24    UnsupportedOptionType {
25        /// The rejected option type.
26        option_type: OptionType,
27    },
28
29    /// The event needs a settlement price and none was supplied.
30    ///
31    /// An expiry needs one to decide moneyness and a cash settlement needs one
32    /// to size its cashflow. A physical explicit exercise or assignment needs
33    /// none.
34    #[error("missing settlement price for a {settlement_type:?} {event:?} event")]
35    MissingPrice {
36        /// The settlement type of the event.
37        settlement_type: SettlementType,
38        /// The event that needs the price.
39        event: SettlementEvent,
40    },
41
42    /// The settlement price was observed after the cutoff.
43    #[error("settlement price observed at {observed_at} is after the cutoff {cutoff}")]
44    PriceAfterCutoff {
45        /// When the price was observed.
46        observed_at: DateTime<Utc>,
47        /// The latest instant a price may be observed at.
48        cutoff: DateTime<Utc>,
49    },
50
51    /// The event cannot happen to a position on this side: only a long
52    /// position exercises and only a short position is assigned.
53    #[error("settlement event {event:?} is incompatible with a {side} position")]
54    EventSideMismatch {
55        /// The requested event.
56        event: SettlementEvent,
57        /// The side of the position.
58        side: Side,
59    },
60
61    /// A figure of the obligation left the representable range.
62    #[error("settlement arithmetic overflow in {operation}: {reason}")]
63    ArithmeticOverflow {
64        /// The computation that overflowed, for example `"physical cash"`.
65        operation: &'static str,
66        /// The underlying arithmetic error, with its operands.
67        reason: String,
68    },
69}
70
71#[cfg(test)]
72mod tests_settlement_error {
73    use super::*;
74    use chrono::TimeZone;
75
76    #[test]
77    fn test_settlement_error_unsupported_option_type_names_the_type() {
78        let error = SettlementError::UnsupportedOptionType {
79            option_type: OptionType::Cliquet {
80                reset_dates: Vec::new(),
81            },
82        };
83        assert_eq!(
84            error.to_string(),
85            "unsupported option type for settlement: Cliquet Option (Reset Dates: []) (supported: European, American)"
86        );
87    }
88
89    #[test]
90    fn test_settlement_error_missing_price_names_the_event() {
91        let error = SettlementError::MissingPrice {
92            settlement_type: SettlementType::Physical,
93            event: SettlementEvent::Expiry,
94        };
95        assert_eq!(
96            error.to_string(),
97            "missing settlement price for a Physical Expiry event"
98        );
99    }
100
101    #[test]
102    fn test_settlement_error_price_after_cutoff_names_both_instants() {
103        let cutoff = Utc.timestamp_opt(1_000, 0).single();
104        let observed_at = Utc.timestamp_opt(2_000, 0).single();
105        assert!(cutoff.is_some() && observed_at.is_some());
106        if let (Some(cutoff), Some(observed_at)) = (cutoff, observed_at) {
107            let text = SettlementError::PriceAfterCutoff {
108                observed_at,
109                cutoff,
110            }
111            .to_string();
112            assert!(text.contains(&observed_at.to_string()));
113            assert!(text.contains(&cutoff.to_string()));
114        }
115    }
116
117    #[test]
118    fn test_settlement_error_event_side_mismatch_names_event_and_side() {
119        let error = SettlementError::EventSideMismatch {
120            event: SettlementEvent::Assignment,
121            side: Side::Long,
122        };
123        assert_eq!(
124            error.to_string(),
125            "settlement event Assignment is incompatible with a Long position"
126        );
127    }
128
129    #[test]
130    fn test_settlement_error_overflow_names_the_operation() {
131        let error = SettlementError::ArithmeticOverflow {
132            operation: "physical cash",
133            reason: "out of range".to_string(),
134        };
135        assert_eq!(
136            error.to_string(),
137            "settlement arithmetic overflow in physical cash: out of range"
138        );
139    }
140}