From 557c5252690af32bd2f5a0c39bf16a5fc596c94d Mon Sep 17 00:00:00 2001 From: Joaquin Bejar Date: Sun, 19 Apr 2026 11:06:24 +0200 Subject: [PATCH 01/10] M4 #334: add # Errors sections to model/ pub fns Added concise # Errors rustdoc sections to all 34 public Result-returning functions across src/model/: - decimal.rs: decimal_to_f64, f64_to_decimal (2) - option.rs: time_to_expiration, calculate_price_binomial_tree, calculate_price_black_scholes, calculate_price_telegraph, payoff, payoff_at_price, intrinsic_value, time_value, calculate_implied_volatility (9) - position.rs: total_cost, premium_received, net_premium_received, pnl_at_expiration, unrealized_pnl, days_held, days_to_expiration, net_cost, fees (9) - trade.rs: TradeStatusAble 6 transitions and 3 Trade accessor methods (9) - leg/traits.rs: delta, gamma, theta, vega, rho (5) - profit_range.rs: ProfitRange::new (1) Each section names the concrete error variant(s) produced or propagated with intra-doc links. Refs #334 --- src/model/decimal.rs | 12 ++++++++ src/model/leg/traits.rs | 35 +++++++++++++++++++++ src/model/option.rs | 65 +++++++++++++++++++++++++++++++++++++++ src/model/position.rs | 59 +++++++++++++++++++++++++++++++++++ src/model/profit_range.rs | 7 +++++ src/model/trade.rs | 51 ++++++++++++++++++++++++++++++ 6 files changed, 229 insertions(+) diff --git a/src/model/decimal.rs b/src/model/decimal.rs index b98311c1..8104187a 100644 --- a/src/model/decimal.rs +++ b/src/model/decimal.rs @@ -139,6 +139,12 @@ impl DecimalStats for Vec { /// * `Result` - The converted f64 value if successful, or a DecimalError /// if the conversion fails /// +/// # Errors +/// +/// Returns [`DecimalError::ConversionError`] when the `Decimal` operand cannot +/// be represented as an `f64` (e.g. out-of-range magnitude or precision loss +/// beyond the `f64` mantissa). +/// /// # Example /// /// ```rust @@ -177,6 +183,12 @@ pub fn decimal_to_f64(value: Decimal) -> Result { /// * `Result` - The converted Decimal value if successful, or a DecimalError /// if the conversion fails /// +/// # Errors +/// +/// Returns [`DecimalError::ConversionError`] when the `f64` operand is not +/// representable as a `Decimal`, for example `NaN`, `±Infinity`, or a value +/// whose magnitude exceeds the `Decimal` range. +/// /// # Example /// /// ```rust diff --git a/src/model/leg/traits.rs b/src/model/leg/traits.rs index d1735871..79a46d61 100644 --- a/src/model/leg/traits.rs +++ b/src/model/leg/traits.rs @@ -69,6 +69,13 @@ pub trait LegAble { /// # Returns /// /// The position delta as a Decimal, or an error if calculation fails. + /// + /// # Errors + /// + /// Propagates any [`GreeksError`] returned by the underlying pricing + /// kernel, typically [`GreeksError::Pricing`] for option legs whose + /// Black–Scholes evaluation fails, or [`GreeksError::ExpirationDate`] + /// when the expiration cannot be resolved. fn delta(&self) -> Result; /// Returns the gamma of this position. @@ -79,6 +86,13 @@ pub trait LegAble { /// # Returns /// /// The position gamma as a Decimal, or an error if calculation fails. + /// + /// # Errors + /// + /// The default implementation is infallible (returns `0`). Option-leg + /// implementors propagate [`GreeksError::Pricing`] on Black–Scholes + /// failure or [`GreeksError::ExpirationDate`] when the expiration is + /// invalid. fn gamma(&self) -> Result { Ok(Decimal::ZERO) } @@ -93,6 +107,13 @@ pub trait LegAble { /// # Returns /// /// The position theta as a Decimal, or an error if calculation fails. + /// + /// # Errors + /// + /// The default implementation is infallible (returns `0`). Option-leg + /// implementors propagate [`GreeksError::Pricing`] when the closed-form + /// time-decay evaluation fails or [`GreeksError::ExpirationDate`] when + /// the expiration cannot be resolved. fn theta(&self) -> Result { Ok(Decimal::ZERO) } @@ -105,6 +126,13 @@ pub trait LegAble { /// # Returns /// /// The position vega as a Decimal, or an error if calculation fails. + /// + /// # Errors + /// + /// The default implementation is infallible (returns `0`). Option-leg + /// implementors propagate [`GreeksError::Pricing`] when the Black–Scholes + /// vega evaluation fails or [`GreeksError::ExpirationDate`] when the + /// expiration cannot be resolved. fn vega(&self) -> Result { Ok(Decimal::ZERO) } @@ -118,6 +146,13 @@ pub trait LegAble { /// # Returns /// /// The position rho as a Decimal, or an error if calculation fails. + /// + /// # Errors + /// + /// The default implementation is infallible (returns `0`). Option-leg + /// implementors propagate [`GreeksError::Pricing`] when the rho + /// evaluation fails or [`GreeksError::ExpirationDate`] when the + /// expiration cannot be resolved. fn rho(&self) -> Result { Ok(Decimal::ZERO) } diff --git a/src/model/option.rs b/src/model/option.rs index 5d80795e..a7a7e222 100644 --- a/src/model/option.rs +++ b/src/model/option.rs @@ -256,6 +256,12 @@ impl Options { /// * `OptionsResult` - A result containing the time to expiration in years /// as a Positive value, or an error if the calculation failed. /// + /// # Errors + /// + /// Propagates any [`expiration_date::error::ExpirationDateError`] returned by + /// [`ExpirationDate::get_years`] (wrapped as [`OptionsError::ExpirationDate`]) + /// when the stored expiration cannot be converted to a positive + /// year fraction (e.g. past expiration or invalid date). pub fn time_to_expiration(&self) -> OptionsResult { Ok(self.expiration_date.get_years()?) } @@ -355,6 +361,13 @@ impl Options { /// /// This method is particularly valuable for pricing American options and other early-exercise /// scenarios that cannot be accurately priced using closed-form solutions. + /// + /// # Errors + /// + /// Returns [`OptionsError::ExpirationDate`] when the option's expiration + /// cannot be converted to a positive year fraction, or propagates any + /// [`PricingError`] surfaced by [`generate_binomial_tree`] (e.g. + /// [`PricingError::BinomialNodeMissing`] or [`PricingError::SqrtFailure`]). pub fn calculate_price_binomial_tree(&self, no_steps: usize) -> PriceBinomialTree { let expiry = self.time_to_expiration()?; let params = BinomialPricingParams { @@ -389,6 +402,14 @@ impl Options { /// /// This method is computationally efficient but limited to European options without /// early exercise capabilities. + /// + /// # Errors + /// + /// Propagates any [`PricingError`] returned by [`black_scholes`] (wrapped as + /// [`OptionsError::PricingError`]), most commonly + /// [`PricingError::ExpirationDate`] when the expiration cannot be resolved + /// or [`PricingError::MethodError`] when the closed-form formula fails + /// numerically. pub fn calculate_price_black_scholes(&self) -> OptionsResult { Ok(black_scholes(self)?) } @@ -430,6 +451,13 @@ impl Options { /// /// * `Result` - A result containing the calculated option price /// as a Decimal value, or a boxed error if the calculation failed. + /// + /// # Errors + /// + /// Propagates any [`PricingError`] returned by [`telegraph`] (wrapped as + /// [`OptionsError::PricingError`]), typically + /// [`PricingError::ExpirationDate`] or [`PricingError::MethodError`] + /// when the finite-difference kernel fails to converge. pub fn calculate_price_telegraph(&self, no_steps: usize) -> OptionsResult { Ok(telegraph(self, no_steps, None, None)?) } @@ -448,6 +476,13 @@ impl Options { /// /// This method is useful for determining the exercise value of an option and for /// analyzing whether an option has intrinsic value. + /// + /// # Errors + /// + /// Currently infallible in practice (the `f64` → `Decimal` conversion falls + /// back to `Decimal::default()`), but the `Result` signature is retained + /// for forward-compatibility with exotic payoff kernels that may surface + /// [`OptionsError`] variants. pub fn payoff(&self) -> OptionsResult { let payoff_info = PayoffInfo { spot: self.underlying_price, @@ -477,6 +512,12 @@ impl Options { /// * `OptionsResult` - The calculated payoff value as a `Decimal`, wrapped in a `Result` type. /// Returns an `Err` if the payoff calculation encounters an error. /// + /// # Errors + /// + /// Currently infallible in practice (the `f64` → `Decimal` conversion falls + /// back to `Decimal::default()`), but the `Result` signature is retained + /// for forward-compatibility with exotic payoff kernels that may surface + /// [`OptionsError`] variants. pub fn payoff_at_price(&self, price: &Positive) -> OptionsResult { let payoff_info = PayoffInfo { spot: *price, @@ -504,6 +545,13 @@ impl Options { /// # Returns /// /// * `OptionsResult` - The intrinsic value of the option, or an error if the calculation fails. + /// + /// # Errors + /// + /// Currently infallible in practice (the `f64` → `Decimal` conversion falls + /// back to `Decimal::default()`), but the `Result` signature is retained + /// for forward-compatibility with exotic payoff kernels that may surface + /// [`OptionsError`] variants. pub fn intrinsic_value(&self, underlying_price: Positive) -> OptionsResult { let payoff_info = PayoffInfo { spot: underlying_price, @@ -547,6 +595,13 @@ impl Options { /// # Returns /// - `Ok(Decimal)` containing the time value (never negative, minimum value is zero) /// - `Err` if the price calculation encounters an error + /// + /// # Errors + /// + /// Propagates any [`OptionsError`] returned by + /// [`Options::calculate_price_black_scholes`] or + /// [`Options::intrinsic_value`] (typically [`OptionsError::PricingError`] + /// with [`PricingError::ExpirationDate`] as the inner cause). pub fn time_value(&self) -> OptionsResult { let option_price = self.calculate_price_black_scholes()?.abs(); let intrinsic_value = self.intrinsic_value(self.underlying_price)?; @@ -668,6 +723,16 @@ impl Options { /// Err(e) => error!("Failed to calculate implied volatility: {:?}", e), /// } /// ``` + /// + /// # Errors + /// + /// Returns [`VolatilityError::PositiveError`] when the midpoint + /// volatility breaches the `Positive` invariant, + /// [`VolatilityError::NoConvergence`] when the bisection exhausts + /// [`MAX_ITERATIONS_IV`] without matching the target price, or propagates + /// [`VolatilityError::Options`] from the underlying Black–Scholes + /// evaluation (wrapped as [`OptionsError::ImpliedVolatilityInvariant`] + /// when the invariant check fails). pub fn calculate_implied_volatility( &self, market_price: Decimal, diff --git a/src/model/position.rs b/src/model/position.rs index eb04ca8c..e96d204f 100644 --- a/src/model/position.rs +++ b/src/model/position.rs @@ -242,6 +242,12 @@ impl Position { /// /// A `f64` representing the total cost of the position. THE VALUE IS ALWAYS POSITIVE /// + /// # Errors + /// + /// Returns [`PositionError`] wrapping a [`PositionValidationErrorKind::InvalidPosition`] + /// when the total cost computation underflows into a negative + /// `Positive`-representable value (typically a short position where the + /// received premium exceeds the declared costs). pub fn total_cost(&self) -> Result { let total_cost = match self.option.side { Side::Long => (self.premium + self.open_fee + self.close_fee) * self.option.quantity, @@ -293,6 +299,13 @@ impl Position { /// # Ok(()) /// # } /// ``` + /// + /// # Errors + /// + /// Returns [`PositionError::invalid_position_type`] for a long position + /// (which never receives a premium), or wraps a `Positive`-conversion + /// failure when `premium × quantity` cannot be represented as a + /// non-negative decimal. pub fn premium_received(&self) -> Result { match self.option.side { Side::Long => Ok(Positive::ZERO), @@ -318,6 +331,12 @@ impl Position { /// - `Err(PositionError)` - If the position is invalid because the premium received /// is less than the costs, resulting in a guaranteed loss /// + /// # Errors + /// + /// Returns [`PositionError`] wrapping a + /// [`PositionValidationErrorKind::InvalidPosition`] when the net + /// amount (premium minus fees) becomes negative, signalling a + /// guaranteed-loss short position. pub fn net_premium_received(&self) -> Result { match self.option.side { Side::Long => Ok(Positive::ZERO), @@ -382,6 +401,12 @@ impl Position { /// # Ok(()) /// # } /// ``` + /// + /// # Errors + /// + /// Propagates any [`OptionsError`] returned by the underlying payoff + /// evaluation ([`Options::intrinsic_value`] or [`Options::payoff`]), + /// wrapped as [`PricingError::OptionError`]. pub fn pnl_at_expiration(&self, price: &Option<&Positive>) -> Result { match price { None => Ok(self.option.intrinsic_value(self.option.underlying_price)? @@ -437,6 +462,13 @@ impl Position { /// # Ok(()) /// # } /// ``` + /// + /// # Errors + /// + /// Returns [`PositionError`] wrapping any + /// [`PositionValidationErrorKind`] surfaced by the internal Black–Scholes + /// evaluation, or [`PositionError::PricingError`] when the + /// implied-volatility recomputation at `price` fails. pub fn unrealized_pnl(&self, price: Positive) -> Result { match self.option.side { Side::Long => Ok((price.to_dec() @@ -465,6 +497,12 @@ impl Position { /// * `Ok(Positive)` - The number of days the position has been held as a positive value /// * `Err(PositionError)` - If there's an error during the calculation or validation /// + /// # Errors + /// + /// Returns [`PositionError`] wrapping a + /// [`PositionValidationErrorKind::InvalidPositionSize`] if the elapsed + /// day-count is negative (future-dated open date) or cannot be + /// represented as a `Positive`. pub fn days_held(&self) -> Result { let days = (Utc::now() - self.date).num_days() as f64; Positive::new(days).map_err(|e| { @@ -487,6 +525,13 @@ impl Position { /// /// For datetime-based expirations, the function calculates the difference between /// the expiration date and the current date, converting the result to days. + /// + /// # Errors + /// + /// Returns [`PositionError`] wrapping the underlying + /// [`expiration_date::error::ExpirationDateError`] when the expiration + /// cannot be converted (e.g. a past datetime that would produce a + /// negative day count). pub fn days_to_expiration(&self) -> Result { match self.option.expiration_date { ExpirationDate::Days(days) => Ok(days), @@ -549,6 +594,13 @@ impl Position { /// A `Decimal` representing the net cost of the position. /// The value should be positive but if the fee is higher than the premium it will be negative /// in short positions + /// + /// # Errors + /// + /// Currently only propagates arithmetic-failure variants surfaced by the + /// premium × quantity and fee additions; in practice infallible for all + /// valid positions, but the `Result` signature is retained to let + /// callers handle future overflow paths uniformly. pub fn net_cost(&self) -> Result { match self.option.side { Side::Long => Ok(self.total_cost()?.to_dec()), @@ -657,6 +709,13 @@ impl Position { /// /// - `Ok(Positive)` - The total fees as a positive value /// - `Err(PositionError)` - If there's an issue calculating the fees + /// + /// # Errors + /// + /// Currently infallible in practice; the `Result` signature is retained + /// so that future fee models that can overflow `Positive` (e.g. tiered + /// fee schedules multiplied by very large quantities) can surface their + /// failures without a breaking API change. pub fn fees(&self) -> Result { Ok((self.open_fee + self.close_fee) * self.option.quantity) } diff --git a/src/model/profit_range.rs b/src/model/profit_range.rs index 7791b4b0..ed2958ea 100644 --- a/src/model/profit_range.rs +++ b/src/model/profit_range.rs @@ -43,6 +43,13 @@ impl ProfitLossRange { /// /// Returns a Result containing the ProfitRange if the boundaries are valid, /// or an error if the boundaries are invalid + /// + /// # Errors + /// + /// Returns [`ProbabilityError`] wrapping a + /// [`ProbabilityCalculationErrorKind::InvalidProbability`] when `lower_bound` + /// is greater than `upper_bound`, or [`ProbabilityCalculationErrorKind::InvalidProbabilityRange`] + /// when the `probability` value lies outside the closed interval `[0, 1]`. pub fn new( lower_bound: Option, upper_bound: Option, diff --git a/src/model/trade.rs b/src/model/trade.rs index e01369bb..0166e5bb 100644 --- a/src/model/trade.rs +++ b/src/model/trade.rs @@ -60,21 +60,56 @@ pub enum TradeStatus { pub trait TradeStatusAble { /// - `open`: Return a `Trade` instance representing the trade in its open status or a /// TradeError if the transition is invalid. + /// + /// # Errors + /// + /// Returns [`TradeError::InvalidTrade`] when the current state cannot + /// transition to the `Open` status (for example, attempting to re-open a + /// trade that has already been closed, exercised, or assigned). fn open(&self) -> Result; /// - `closed`: Return a `Trade` instance representing the trade in its closed status or a /// TradeError if the transition is invalid. + /// + /// # Errors + /// + /// Returns [`TradeError::InvalidTrade`] when the trade is not currently + /// in a state that allows transitioning to `Closed` (e.g. a trade that + /// has not been opened yet). fn close(&self) -> Result; /// - `expired`: Return a `Trade` instance representing the trade in its expired status or a /// TradeError if the transition is invalid. + /// + /// # Errors + /// + /// Returns [`TradeError::InvalidTrade`] when the trade is not in a state + /// that allows transitioning to `Expired` (e.g. already closed or + /// exercised). fn expired(&self) -> Result; /// - `exercised`: Return a `Trade` instance representing the trade in its exercised status or a /// TradeError if the transition is invalid. + /// + /// # Errors + /// + /// Returns [`TradeError::InvalidTrade`] when the option cannot legally + /// be exercised from the current trade state (e.g. the trade has + /// already expired or been assigned). fn exercised(&self) -> Result; /// - `assigned`: Return a `Trade` instance representing the trade in its assigned status or a /// TradeError if the transition is invalid. + /// + /// # Errors + /// + /// Returns [`TradeError::InvalidTrade`] when the trade cannot be moved + /// into the `Assigned` state from its current status. fn assigned(&self) -> Result; /// - `status_other`: Return a `Trade` instance representing undeclared status or a /// TradeError if the transition is invalid. + /// + /// # Errors + /// + /// Returns [`TradeError::InvalidTrade`] when the implementation rejects + /// the transition to a non-canonical status (the semantics are left to + /// the implementor). fn status_other(&self) -> Result; } @@ -409,6 +444,12 @@ pub trait TradeAble { /// It ensures that the `Trade` is available for viewing or interaction. /// /// Note: This method returns an owned `Trade` instance, allowing modification. + /// + /// # Errors + /// + /// Returns [`TradeError::InvalidTrade`] when the implementor cannot + /// materialize a `Trade` from its current state (typically when the + /// container does not yet hold a committed trade). fn trade(&self) -> Result; /// Returns a reference to the `Trade` associated with the current instance. @@ -419,6 +460,11 @@ pub trait TradeAble { /// /// # Note /// The returned reference has the same lifetime as the instance it is called on. + /// + /// # Errors + /// + /// Returns [`TradeError::InvalidTrade`] when the implementor does not + /// currently hold a materialized `Trade` to borrow. fn trade_ref(&self) -> Result<&Trade, TradeError>; /// Provides a mutable reference to the `Trade` instance contained within the current structure. @@ -434,6 +480,11 @@ pub trait TradeAble { /// - Since this method provides a mutable reference, it enforces Rust's borrow rules. /// Only one mutable reference to the `Trade` is allowed at a time. /// - Ensure that concurrent access to the structure is properly managed to avoid runtime issues. + /// + /// # Errors + /// + /// Returns [`TradeError::InvalidTrade`] when the implementor does not + /// currently hold a materialized `Trade` to borrow mutably. fn trade_mut(&mut self) -> Result<&mut Trade, TradeError>; } From fc87fc12634c7aac06aff2ef9e2b06cbbb5121b3 Mon Sep 17 00:00:00 2001 From: Joaquin Bejar Date: Sun, 19 Apr 2026 11:08:00 +0200 Subject: [PATCH 02/10] M4 #334: add # Errors sections to chains/ pub fns Added concise # Errors rustdoc sections to 14 public Result-returning functions across src/chains/: - chain.rs: to_build_params, atm_strike, save_to_csv, save_to_csv_async, save_to_json, save_to_json_async, load_from_csv, load_from_csv_async, load_from_json, load_from_json_async, get_random_positions, set_optiondata_extra_params (12) - rnd.rs: RNDAnalysis::calculate_rnd, calculate_skew (2) Each section references concrete ChainError variants with intra-doc links and mentions the spawn_blocking join-failure path for async wrappers. Refs #334 --- src/chains/chain.rs | 79 +++++++++++++++++++++++++++++++++++++++++++++ src/chains/rnd.rs | 14 ++++++++ 2 files changed, 93 insertions(+) diff --git a/src/chains/chain.rs b/src/chains/chain.rs index 8794b8a6..85eb7d1b 100644 --- a/src/chains/chain.rs +++ b/src/chains/chain.rs @@ -558,6 +558,12 @@ impl OptionChain { /// this option chain. The method calculates appropriate values for chain size, strike interval, /// and estimated spread based on the current data. /// + /// # Errors + /// + /// Returns [`ChainError::ChainBuildError`] when the chain is empty, + /// when no valid strike interval can be inferred from existing + /// strikes, or when the volatility-surface sampler fails to produce a + /// skew for the generated parameters. pub fn to_build_params(&self) -> Result { // Calculate chain size based on the distance from ATM strike let atm_strike = self.atm_strike()?; @@ -882,6 +888,12 @@ impl OptionChain { /// Err(e) => error!("Error finding ATM strike: {}", e), /// } /// ``` + /// + /// # Errors + /// + /// Returns [`ChainError::EmptyChainAtm`] when the chain contains no + /// options, or [`ChainError::AtmNotFound`] when no strike is close + /// enough to the underlying to be considered at-the-money. pub fn atm_strike(&self) -> Result<&Positive, ChainError> { let option_data = self.atm_option_data()?; Ok(&option_data.strike_price) @@ -1069,6 +1081,12 @@ impl OptionChain { /// # Note /// /// This method is only available on non-WebAssembly targets. + /// + /// # Errors + /// + /// Returns [`ChainError::FileError`] wrapping a [`FileErrorKind::IOError`] + /// when the file cannot be created or written, or + /// [`FileErrorKind::ParseError`] when `csv` serialization fails. pub fn save_to_csv(&self, file_path: &str) -> Result<(), ChainError> { let full_path = format!("{}/{}.csv", file_path, self.get_title()); let mut wtr = WriterBuilder::new().from_path(full_path)?; @@ -1109,6 +1127,13 @@ impl OptionChain { /// # Note /// /// This method is only available on non-WebAssembly targets with the `async` feature. + /// + /// # Errors + /// + /// Returns the same variants as [`OptionChain::save_to_csv`] + /// ([`ChainError::FileError`] wrapping [`FileErrorKind::IOError`] or + /// [`FileErrorKind::ParseError`]). A `spawn_blocking` join failure is + /// surfaced as [`FileErrorKind::IOError`]. #[cfg(feature = "async")] pub async fn save_to_csv_async(&self, file_path: &str) -> Result<(), ChainError> { let path = file_path.to_string(); @@ -1136,6 +1161,12 @@ impl OptionChain { /// # Note /// /// This method is only available on non-WebAssembly targets. + /// + /// # Errors + /// + /// Returns [`ChainError::FileError`] wrapping a [`FileErrorKind::IOError`] + /// when the file cannot be created or written, or + /// [`FileErrorKind::ParseError`] when `serde_json` serialization fails. pub fn save_to_json(&self, file_path: &str) -> Result<(), ChainError> { let full_path = format!("{}/{}.json", file_path, self.get_title()); let file = File::create(full_path)?; @@ -1148,6 +1179,13 @@ impl OptionChain { /// # Note /// /// This method is only available on non-WebAssembly targets with the `async` feature. + /// + /// # Errors + /// + /// Returns the same variants as [`OptionChain::save_to_json`] + /// ([`ChainError::FileError`] wrapping [`FileErrorKind::IOError`] or + /// [`FileErrorKind::ParseError`]). A `spawn_blocking` join failure is + /// surfaced as [`FileErrorKind::IOError`]. #[cfg(feature = "async")] pub async fn save_to_json_async(&self, file_path: &str) -> Result<(), ChainError> { let path = file_path.to_string(); @@ -1174,6 +1212,14 @@ impl OptionChain { /// # Note /// /// This method is only available on non-WebAssembly targets. + /// + /// # Errors + /// + /// Returns [`ChainError::FileError`] wrapping [`FileErrorKind::IOError`] + /// when the CSV file cannot be opened or read, or + /// [`FileErrorKind::ParseError`] when the CSV records cannot be parsed. + /// Invalid option data (bad strike, volatility or price) surfaces as + /// [`ChainError::OptionDataError`]. pub fn load_from_csv(file_path: &str) -> Result { let mut rdr = csv::Reader::from_path(file_path)?; let mut options = BTreeSet::new(); @@ -1227,6 +1273,12 @@ impl OptionChain { /// # Note /// /// This method is only available on non-WebAssembly targets with the `async` feature. + /// + /// # Errors + /// + /// Returns the same variants as [`OptionChain::load_from_csv`]. A + /// `spawn_blocking` join failure is surfaced as + /// [`ChainError::FileError`] wrapping [`FileErrorKind::IOError`]. #[cfg(feature = "async")] pub async fn load_from_csv_async(file_path: &str) -> Result { let path = file_path.to_string(); @@ -1252,6 +1304,12 @@ impl OptionChain { /// # Note /// /// This method is only available on non-WebAssembly targets. + /// + /// # Errors + /// + /// Returns [`ChainError::FileError`] wrapping [`FileErrorKind::IOError`] + /// when the file cannot be opened, or [`FileErrorKind::ParseError`] + /// when `serde_json` deserialization fails. pub fn load_from_json(file_path: &str) -> Result { let file = File::open(file_path)?; let mut option_chain: OptionChain = serde_json::from_reader(file)?; @@ -1276,6 +1334,12 @@ impl OptionChain { /// # Note /// /// This method is only available on non-WebAssembly targets with the `async` feature. + /// + /// # Errors + /// + /// Returns the same variants as [`OptionChain::load_from_json`]. A + /// `spawn_blocking` join failure is surfaced as + /// [`ChainError::FileError`] wrapping [`FileErrorKind::IOError`]. #[cfg(feature = "async")] pub async fn load_from_json_async(file_path: &str) -> Result { let path = file_path.to_string(); @@ -1363,6 +1427,14 @@ impl OptionChain { /// # Returns /// /// * `Result, ChainError>` - Vector of created positions or error message + /// + /// # Errors + /// + /// Returns [`ChainError::StrategyError`] wrapping a + /// [`StrategyErrorKind::InvalidLegs`] when the requested position counts + /// exceed available strikes on either side of the chain, or propagates + /// any [`ChainError::OptionDataError`] produced while materialising the + /// selected strikes into [`Position`] instances. pub fn get_random_positions( &self, params: RandomPositionsParams, @@ -2842,6 +2914,13 @@ impl OptionChain { /// /// # Returns /// * `Result<(), ChainError>` - Ok if successful, or an error if the operation fails. + /// + /// # Errors + /// + /// Returns [`ChainError::ExpirationDate`] when the chain's expiration + /// string cannot be parsed, or propagates any + /// [`ChainError::OptionDataError`] surfaced while enriching individual + /// [`OptionData`] entries with extra pricing parameters. pub fn set_optiondata_extra_params(&mut self) -> Result<(), ChainError> { let params = OptionDataPriceParams::new( Some(Box::new(self.underlying_price)), diff --git a/src/chains/rnd.rs b/src/chains/rnd.rs index 0ce34877..1570d65b 100644 --- a/src/chains/rnd.rs +++ b/src/chains/rnd.rs @@ -409,6 +409,13 @@ pub trait RNDAnalysis { /// /// # Returns /// Result containing either RNDResult or an error + /// + /// # Errors + /// + /// Returns [`ChainError::EmptyDensities`] when no valid density values + /// can be extracted from the chain, or [`ChainError::OptionDataError`] + /// when individual strikes produce numerical failures during the + /// finite-difference second-derivative approximation. fn calculate_rnd(&self, params: &RNDParameters) -> Result; /// Calculates the implied volatility skew @@ -418,6 +425,13 @@ pub trait RNDAnalysis { /// /// # Returns /// Result containing vector of (strike_price, volatility) pairs or an error + /// + /// # Errors + /// + /// Returns [`ChainError::EmptySkewData`] when no strike in the chain + /// produced a valid implied-volatility sample, or + /// [`ChainError::OptionDataError`] if individual option records carry + /// invalid volatility values. fn calculate_skew(&self) -> Result, ChainError>; } From a207dbb9eb11984d22e651e7619376e1cba33ea3 Mon Sep 17 00:00:00 2001 From: Joaquin Bejar Date: Sun, 19 Apr 2026 11:12:47 +0200 Subject: [PATCH 03/10] M4 #334: add # Errors sections to strategies/ pub fns Added concise # Errors rustdoc sections to 46 public Result-returning functions and trait methods across src/strategies/: - base.rs: 18 Strategable / BreakEvenable / PositionAble trait methods - build/traits.rs: get_strategy (1) - collar.rs: net_delta, max_profit_potential, max_loss_potential (3) - covered_call.rs: net_delta, max_profit_potential, max_loss_potential (3) - protective_put.rs: net_delta, max_loss_potential (2) - delta_neutral/model.rs: 9 DeltaNeutrality trait methods - delta_neutral/optimizer.rs: optimize (1) - delta_neutral/portfolio.rs: from_positions, from_positions_with_underlying (2) - probabilities/core.rs: 7 ProbabilityAnalysis trait methods - shared.rs: calculate_profit_ratio (1) Each section references concrete error variants with intra-doc links and calls out propagated errors versus variants produced locally. Refs #334 --- src/strategies/base.rs | 118 ++++++++++++++++++++++ src/strategies/build/traits.rs | 6 ++ src/strategies/collar.rs | 21 ++++ src/strategies/covered_call.rs | 19 ++++ src/strategies/delta_neutral/model.rs | 65 ++++++++++++ src/strategies/delta_neutral/optimizer.rs | 8 ++ src/strategies/delta_neutral/portfolio.rs | 13 +++ src/strategies/probabilities/core.rs | 51 ++++++++++ src/strategies/protective_put.rs | 13 +++ src/strategies/shared.rs | 8 ++ 10 files changed, 322 insertions(+) diff --git a/src/strategies/base.rs b/src/strategies/base.rs index a630c521..b8b00fda 100644 --- a/src/strategies/base.rs +++ b/src/strategies/base.rs @@ -67,6 +67,14 @@ pub trait Strategable: /// /// A `Result` containing the `StrategyBasics` struct if successful, or a `StrategyError` /// if the operation is not supported. + /// + /// # Errors + /// + /// The default implementation returns [`StrategyError::OperationError`] + /// with [`OperationErrorKind::NotSupported`]; concrete strategies that + /// override it may surface [`StrategyError::PriceError`] or + /// [`StrategyError::BreakEvenError`] when the underlying computations + /// fail. fn info(&self) -> Result { Err(StrategyError::operation_not_supported( "info", @@ -809,6 +817,13 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// # Returns /// * `Ok(Positive)` - The maximum possible profit. /// * `Err(StrategyError)` - If the operation is not supported for this strategy. + /// + /// # Errors + /// + /// The default implementation returns [`StrategyError::OperationError`] + /// with [`OperationErrorKind::NotSupported`]. Concrete strategies may + /// surface [`StrategyError::PriceError`] or + /// [`StrategyError::ProfitLossError`] when the payoff evaluation fails. fn get_max_profit(&self) -> Result { Err(StrategyError::operation_not_supported( "max_profit", @@ -822,6 +837,11 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// # Returns /// * `Ok(Positive)` - The maximum possible profit. /// * `Err(StrategyError)` - If the operation is not supported for this strategy. + /// + /// # Errors + /// + /// Propagates any [`StrategyError`] returned by + /// [`Strategable::get_max_profit`] on `&self`. fn get_max_profit_mut(&mut self) -> Result { self.get_max_profit() } @@ -832,6 +852,13 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// # Returns /// * `Ok(Positive)` - The maximum possible loss. /// * `Err(StrategyError)` - If the operation is not supported for this strategy. + /// + /// # Errors + /// + /// The default implementation returns [`StrategyError::OperationError`] + /// with [`OperationErrorKind::NotSupported`]. Concrete strategies may + /// surface [`StrategyError::PriceError`] or + /// [`StrategyError::ProfitLossError`] when the payoff evaluation fails. fn get_max_loss(&self) -> Result { Err(StrategyError::operation_not_supported( "max_loss", @@ -845,6 +872,11 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// # Returns /// * `Ok(Positive)` - The maximum possible loss. /// * `Err(StrategyError)` - If the operation is not supported for this strategy. + /// + /// # Errors + /// + /// Propagates any [`StrategyError`] returned by + /// [`Strategable::get_max_loss`] on `&self`. fn get_max_loss_mut(&mut self) -> Result { self.get_max_loss() } @@ -854,6 +886,13 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// # Returns /// * `Ok(Positive)` - The total cost of the strategy. /// * `Err(PositionError)` - If there is an error retrieving the positions. + /// + /// # Errors + /// + /// Propagates any [`PositionError`] returned by + /// [`Strategable::get_positions`] or by + /// [`Position::total_cost`] when the component legs surface invalid + /// state. fn get_total_cost(&self) -> Result { let positions = self.get_positions()?; let mut total = Positive::ZERO; @@ -869,6 +908,13 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// # Returns /// * `Ok(Decimal)` - The net cost of the strategy. /// * `Err(PositionError)` - If there is an error retrieving the positions. + /// + /// # Errors + /// + /// Propagates any [`PositionError`] returned by + /// [`Strategable::get_positions`] or by + /// [`Position::net_cost`] when the component legs surface invalid + /// state. fn get_net_cost(&self) -> Result { let positions = self.get_positions()?; let mut total = Decimal::ZERO; @@ -884,6 +930,12 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// # Returns /// * `Ok(Positive)` - The net premium received. /// * `Err(StrategyError)` - If there is an error retrieving the positions. + /// + /// # Errors + /// + /// Returns [`StrategyError::from(PositionError)`] when + /// [`Strategable::get_positions`] or + /// [`Position::net_premium_received`] fail on any leg. fn get_net_premium_received(&self) -> Result { let positions = self.get_positions()?; let mut costs = Decimal::ZERO; @@ -906,6 +958,12 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// # Returns /// * `Ok(Positive)` - The total fees. /// * `Err(StrategyError)` - If there is an error retrieving positions or calculating fees. + /// + /// # Errors + /// + /// Returns [`StrategyError::from(PositionError)`] when + /// [`Strategable::get_positions`] or [`Position::fees`] fail on any + /// leg. fn get_fees(&self) -> Result { let mut fee = Positive::ZERO; let positions = match self.get_positions() { @@ -931,6 +989,13 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// # Returns /// * `Ok(Decimal)` - The profit area. /// * `Err(StrategyError)` - If the operation is not supported. + /// + /// # Errors + /// + /// The default implementation returns [`StrategyError::OperationError`] + /// with [`OperationErrorKind::NotSupported`]. Overriding strategies may + /// surface [`StrategyError::BreakEvenError`] or + /// [`StrategyError::PriceError`] when the payoff integral fails. fn get_profit_area(&self) -> Result { Err(StrategyError::operation_not_supported( "profit_area", @@ -944,6 +1009,13 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// # Returns /// * `Ok(Decimal)` - The profit ratio. /// * `Err(StrategyError)` - If the operation is not supported. + /// + /// # Errors + /// + /// The default implementation returns [`StrategyError::OperationError`] + /// with [`OperationErrorKind::NotSupported`]. Overriding strategies may + /// surface [`StrategyError::ProfitLossError`] when either + /// `get_max_profit` or `get_max_loss` fails. fn get_profit_ratio(&self) -> Result { Err(StrategyError::operation_not_supported( "profit_ratio", @@ -959,6 +1031,12 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// # Returns /// * `Ok((Positive, Positive))` - A tuple containing the start and end prices of the range. /// * `Err(StrategyError)` - If there is an error retrieving necessary data for the calculation. + /// + /// # Errors + /// + /// Propagates any [`StrategyError`] returned by + /// [`Strategable::get_break_even_points`] or + /// [`Strategable::get_max_min_strikes`]. fn get_range_to_show(&self) -> Result<(Positive, Positive), StrategyError> { let mut all_points = self.get_break_even_points()?.clone(); let (first_strike, last_strike) = self.get_max_min_strikes()?; @@ -997,6 +1075,11 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// # Returns /// * `Ok(Vec)` - A vector of prices. /// * `Err(StrategyError)` - If there is an error calculating the display range. + /// + /// # Errors + /// + /// Propagates any [`StrategyError`] returned by + /// [`Strategable::get_range_to_show`]. fn get_best_range_to_show(&self, step: Positive) -> Result, StrategyError> { let (start_price, end_price) = self.get_range_to_show()?; Ok(calculate_price_range(start_price, end_price, step)) @@ -1008,6 +1091,12 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// # Returns /// * `Ok((Positive, Positive))` - A tuple containing the minimum and maximum strike prices. /// * `Err(StrategyError)` - If no strikes are found or if an error occurs retrieving positions. + /// + /// # Errors + /// + /// Returns [`StrategyError::PriceError`] when the strategy has no + /// strikes to compare against; propagates [`StrategyError`] variants + /// from [`Strategable::get_positions`] when position enumeration fails. fn get_max_min_strikes(&self) -> Result<(Positive, Positive), StrategyError> { let strikes: Vec<&Positive> = self.get_strikes(); if strikes.is_empty() { @@ -1048,6 +1137,12 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// * `Ok(Positive)` - The difference between the highest and lowest break-even points. Returns /// `Positive::INFINITY` if there is only one break-even point. /// * `Err(StrategyError)` - if there are no break-even points. + /// + /// # Errors + /// + /// Returns [`StrategyError::BreakEvenError`] when the strategy has + /// no break-even points, and propagates any other [`StrategyError`] + /// surfaced by [`Strategable::get_break_even_points`]. fn get_range_of_profit(&self) -> Result { let mut break_even_points = self.get_break_even_points()?.clone(); match break_even_points.len() { @@ -1139,6 +1234,13 @@ pub trait BreakEvenable { /// /// The default implementation returns a `StrategyError::OperationError` with `OperationErrorKind::NotSupported`. /// Strategies implementing this trait should override this method if they support break-even point calculations. + /// + /// # Errors + /// + /// The default implementation returns [`StrategyError::OperationError`] + /// with [`OperationErrorKind::NotSupported`]. Concrete strategies may + /// surface [`StrategyError::BreakEvenError`] when no crossing is + /// found in the payoff profile. fn get_break_even_points(&self) -> Result<&Vec, StrategyError> { Err(StrategyError::operation_not_supported( "get_break_even_points", @@ -1368,6 +1470,14 @@ pub trait Positionable { /// /// The default implementation returns an error indicating that adding a position is not /// supported. Strategies that support adding positions should override this method. + /// + /// # Errors + /// + /// The default implementation returns + /// [`PositionError::unsupported_operation`]. Overriding strategies may + /// surface [`PositionError::ValidationError`] when the added position + /// violates strategy invariants (e.g. mismatched underlying, wrong + /// side or invalid quantity). fn add_position(&mut self, _position: &Position) -> Result<(), PositionError> { Err(PositionError::unsupported_operation( std::any::type_name::(), @@ -1387,6 +1497,14 @@ pub trait Positionable { /// /// The default implementation returns an error indicating that getting positions is not /// supported. Strategies that manage positions should override this method. + /// + /// # Errors + /// + /// The default implementation returns + /// [`PositionError::unsupported_operation`]. Overriding strategies + /// typically do not fail, but may surface + /// [`PositionError::ValidationError`] if the internal layout has been + /// corrupted. fn get_positions(&self) -> Result, PositionError> { Err(PositionError::unsupported_operation( std::any::type_name::(), diff --git a/src/strategies/build/traits.rs b/src/strategies/build/traits.rs index 314ea447..08f57ce5 100644 --- a/src/strategies/build/traits.rs +++ b/src/strategies/build/traits.rs @@ -41,6 +41,12 @@ pub trait StrategyConstructor: Strategies + Greeks { /// * `Ok(Self)` - The successfully constructed strategy /// * `Err(StrategyError)` - If the positions don't match the expected /// pattern for this strategy type + /// + /// # Errors + /// + /// Returns [`StrategyError::StrategyInvalid`] when the provided + /// positions do not match the leg count or side/style pattern + /// required by the implementing strategy type. fn get_strategy(_vec_positions: &[Position]) -> Result where Self: Sized, diff --git a/src/strategies/collar.rs b/src/strategies/collar.rs index 063a36fa..7091e6a7 100644 --- a/src/strategies/collar.rs +++ b/src/strategies/collar.rs @@ -360,6 +360,11 @@ impl Collar { /// Calculates the net delta of the collar. /// /// Net Delta = Spot Delta + Put Delta + Call Delta + /// + /// # Errors + /// + /// Propagates any [`GreeksError`] returned by + /// [`LegAble::delta`] on the spot leg, long-put leg or short-call leg. pub fn net_delta(&self) -> Result { let spot_delta = self.spot_leg.delta()?; let put_delta = self.long_put.delta()?; @@ -370,6 +375,14 @@ impl Collar { /// Calculates the maximum profit potential. /// /// Max Profit = (Call Strike - Cost Basis) × Quantity + Net Premium - Fees + /// + /// # Errors + /// + /// Returns [`PricingError::MethodError`] with method `collar` when + /// the short-call strike is below the spot cost basis (the strategy + /// is not economically well-formed). Arithmetic follows + /// [`PricingError::ArithmeticFailure`] if fee or premium conversions + /// overflow. pub fn max_profit_potential(&self) -> Result { let call_strike = self.call_strike(); let cost_basis = self.spot_leg.cost_basis; @@ -392,6 +405,14 @@ impl Collar { /// Calculates the maximum loss potential. /// /// Max Loss = (Cost Basis - Put Strike) × Quantity - Net Premium + Fees + /// + /// # Errors + /// + /// Returns [`PricingError::MethodError`] with method `collar` when + /// the loss decomposition underflows (for example when the put strike + /// equals or exceeds the spot cost basis and the net premium net of + /// fees is positive), or when arithmetic fails while converting + /// `Positive` operands. pub fn max_loss_potential(&self) -> Result { let put_strike = self.put_strike(); let cost_basis = self.spot_leg.cost_basis; diff --git a/src/strategies/covered_call.rs b/src/strategies/covered_call.rs index 7e83e3e9..a25ed3de 100644 --- a/src/strategies/covered_call.rs +++ b/src/strategies/covered_call.rs @@ -263,6 +263,11 @@ impl CoveredCall { /// /// Net Delta = Spot Delta + Option Delta /// For a covered call: typically positive but less than 1.0 per share + /// + /// # Errors + /// + /// Propagates any [`GreeksError`] returned by + /// [`LegAble::delta`] on the spot leg or the short-call leg. pub fn net_delta(&self) -> Result { let spot_delta = self.spot_leg.delta()?; let option_delta = self.short_call.delta()?; @@ -286,6 +291,13 @@ impl CoveredCall { /// Calculates the maximum profit potential. /// /// Max Profit = (Strike - Cost Basis) × Quantity + Premium Received - Fees + /// + /// # Errors + /// + /// Returns [`PricingError::MethodError`] with method `covered_call` + /// when the call strike sits below the spot cost basis (the strategy + /// has no upside), or when arithmetic on `Positive` operands + /// overflows. pub fn max_profit_potential(&self) -> Result { let strike = self.call_strike(); let cost_basis = self.spot_leg.cost_basis; @@ -312,6 +324,13 @@ impl CoveredCall { /// /// Max Loss = Cost Basis × Quantity - Premium Received + Fees /// (occurs if underlying goes to zero) + /// + /// # Errors + /// + /// Returns [`PricingError::MethodError`] with method `covered_call` + /// when the premium net of fees exceeds `cost_basis × quantity` + /// (which would imply a non-negative worst case), or when arithmetic + /// on `Positive` operands overflows. pub fn max_loss_potential(&self) -> Result { let cost_basis = self.spot_leg.cost_basis; let quantity = self.spot_leg.quantity; diff --git a/src/strategies/delta_neutral/model.rs b/src/strategies/delta_neutral/model.rs index 7ba3ba4e..fdd62ae2 100644 --- a/src/strategies/delta_neutral/model.rs +++ b/src/strategies/delta_neutral/model.rs @@ -349,6 +349,15 @@ pub trait DeltaNeutrality: Greeks + Positionable + Strategies { /// * The current price of the underlying asset. /// /// This provides an overview of the delta position and helps in determining adjustments. + /// + /// # Errors + /// + /// Propagates any [`GreeksError`] surfaced by + /// [`Strategable::get_options`] or by the per-option + /// [`Greeks::delta`] evaluation, and returns + /// [`GreeksError::DeltaNeutrality`] with + /// [`DeltaNeutralityErrorKind::NotAchievable`] when the portfolio + /// aggregation itself fails a sanity check. fn delta_neutrality(&self) -> Result { let options = self.get_options()?; if options.is_empty() { @@ -593,6 +602,12 @@ pub trait DeltaNeutrality: Greeks + Positionable + Strategies { /// - Uses DELTA_THRESHOLD to determine if adjustments are needed /// - Suggests opposite positions to neutralize current delta exposure /// - Accounts for both option style (Put/Call) and position side (Long/Short) + /// + /// # Errors + /// + /// Propagates any [`GreeksError`] returned by + /// [`Greeks::delta`] on individual legs, or by the internal + /// [`delta_neutrality`] computation. fn delta_adjustments(&self) -> Result, GreeksError> { let net_delta = self.delta()?; @@ -721,6 +736,14 @@ pub trait DeltaNeutrality: Greeks + Positionable + Strategies { /// to determine the current state and required actions /// - SameSize adjustments are only applied when no specific action filter is provided /// - Incompatible adjustments for the specified action are skipped with a debug message + /// + /// # Errors + /// + /// Returns [`StrategyError::GreeksError`] when the underlying + /// [`delta_neutrality`] or [`delta_adjustments`] calls fail, and + /// [`StrategyError::PositionError`] (via + /// [`apply_single_adjustment`]) when the adjustment cannot be + /// committed to the strategy. fn apply_delta_adjustments(&mut self, action: Option) -> Result<(), StrategyError> { let delta_info = self.delta_neutrality()?; if delta_info.is_neutral { @@ -801,6 +824,12 @@ pub trait DeltaNeutrality: Greeks + Positionable + Strategies { /// The actual position adjustment is performed by the `adjust_option_position` method, which is /// called with positive quantities for buying options and negative quantities for selling options. /// + /// # Errors + /// + /// Returns [`StrategyError::PositionError`] when the underlying + /// [`adjust_option_position`] rejects the requested quantity change + /// (e.g. strike not found, invalid side for the adjustment, or + /// invariant violation on the resulting position). fn apply_single_adjustment( &mut self, adjustment: &DeltaAdjustment, @@ -893,6 +922,13 @@ pub trait DeltaNeutrality: Greeks + Positionable + Strategies { /// # Returns /// A `Trade` object derived from the delta adjustment logic. /// + /// # Errors + /// + /// Returns [`StrategyError::GreeksError`] when the internal + /// [`delta_adjustments`] call fails, and + /// [`StrategyError::PositionError`] when the adjustment cannot be + /// converted into a committable trade (invalid side, strike not + /// present in the chain, or quantity invariant breach). fn trade_from_delta_adjustment(&mut self, action: Action) -> Result, StrategyError> { let adjustments = self.delta_adjustments()?; let mut trades = Vec::new(); @@ -993,6 +1029,13 @@ pub trait DeltaNeutrality: Greeks + Positionable + Strategies { /// println!("Portfolio delta: {}", greeks.delta); /// println!("Portfolio gamma: {}", greeks.gamma); /// ``` + /// + /// # Errors + /// + /// Propagates any [`GreeksError`] surfaced by + /// [`Strategable::get_positions`] or by + /// [`PortfolioGreeks::from_positions`], typically + /// [`GreeksError::Pricing`] when individual Black–Scholes legs fail. fn portfolio_greeks(&self) -> Result { let positions: Vec<_> = self .get_positions() @@ -1033,6 +1076,14 @@ pub trait DeltaNeutrality: Greeks + Positionable + Strategies { /// let plan = strategy.optimized_adjustment_plan(config, target)?; /// println!("Actions needed: {}", plan.actions.len()); /// ``` + /// + /// # Errors + /// + /// Returns [`StrategyError::GreeksError`] when the portfolio Greeks + /// cannot be computed, or [`StrategyError::Adjustment`] (mapped from + /// [`AdjustmentError`]) when the optimiser cannot find a viable plan + /// for the provided `config` and `target` (e.g. no positions, + /// infeasible constraints, or cost ceiling breached). fn optimized_adjustment_plan( &self, config: AdjustmentConfig, @@ -1063,6 +1114,14 @@ pub trait DeltaNeutrality: Greeks + Positionable + Strategies { /// /// * `Ok(AdjustmentPlan)` - The optimal adjustment plan /// * `Err(StrategyError)` - If no viable plan can be found + /// + /// # Errors + /// + /// Same failure surface as + /// [`optimized_adjustment_plan`] plus additional chain-driven + /// failures: returns [`StrategyError::Adjustment`] mapped from + /// [`AdjustmentError`] when the chain does not expose any candidate + /// strike that satisfies the target delta direction. fn optimized_adjustment_plan_with_chain( &self, chain: &crate::chains::chain::OptionChain, @@ -1106,6 +1165,12 @@ pub trait DeltaNeutrality: Greeks + Positionable + Strategies { /// /// * `Ok(Decimal)` - The gap between current and target delta /// * `Err(GreeksError)` - If delta calculation fails + /// + /// # Errors + /// + /// Propagates any [`GreeksError`] returned by [`Greeks::delta`] on + /// the aggregate strategy (typically + /// [`GreeksError::Pricing`] for option-leg Black–Scholes failures). fn delta_gap(&self, target_delta: Decimal) -> Result { let current_delta = self.delta()?; Ok(target_delta - current_delta) diff --git a/src/strategies/delta_neutral/optimizer.rs b/src/strategies/delta_neutral/optimizer.rs index 3ff32ad2..103a7b0c 100644 --- a/src/strategies/delta_neutral/optimizer.rs +++ b/src/strategies/delta_neutral/optimizer.rs @@ -100,6 +100,14 @@ impl<'a> AdjustmentOptimizer<'a> { /// /// * `Ok(AdjustmentPlan)` - The optimal adjustment plan /// * `Err(AdjustmentError)` - If no viable plan can be found + /// + /// # Errors + /// + /// Returns [`AdjustmentError::NoPositions`] when `positions` is + /// empty, [`AdjustmentError::InfeasibleTarget`] when no combination + /// of candidate strikes can close the delta gap within the + /// configured tolerance, or [`AdjustmentError::CostCeilingBreached`] + /// when every viable plan exceeds the configured `max_cost`. pub fn optimize(&self) -> Result { if self.positions.is_empty() { return Err(AdjustmentError::NoPositions); diff --git a/src/strategies/delta_neutral/portfolio.rs b/src/strategies/delta_neutral/portfolio.rs index 7d90fb59..4d48aa20 100644 --- a/src/strategies/delta_neutral/portfolio.rs +++ b/src/strategies/delta_neutral/portfolio.rs @@ -95,6 +95,13 @@ impl PortfolioGreeks { /// let greeks = PortfolioGreeks::from_positions(&positions)?; /// println!("Portfolio delta: {}", greeks.delta); /// ``` + /// + /// # Errors + /// + /// Propagates any [`GreeksError`] returned by [`Greeks::delta`], + /// [`Greeks::gamma`], [`Greeks::theta`], [`Greeks::vega`] or + /// [`Greeks::rho`] on the input positions — typically + /// [`GreeksError::Pricing`] when Black–Scholes fails on a leg. pub fn from_positions(positions: &[Position]) -> Result { let mut greeks = Self::default(); @@ -127,6 +134,12 @@ impl PortfolioGreeks { /// # Returns /// /// * `Ok(PortfolioGreeks)` - Aggregated Greeks including underlying + /// + /// # Errors + /// + /// Same failure surface as [`PortfolioGreeks::from_positions`] — + /// only the option legs can fail; the underlying adjustment is pure + /// arithmetic and does not introduce new error paths. pub fn from_positions_with_underlying( positions: &[Position], underlying_quantity: Decimal, diff --git a/src/strategies/probabilities/core.rs b/src/strategies/probabilities/core.rs index 2e8de0a7..c1628960 100644 --- a/src/strategies/probabilities/core.rs +++ b/src/strategies/probabilities/core.rs @@ -66,6 +66,14 @@ pub trait ProbabilityAnalysis: Strategies + Profit { /// - Expected value /// - Break-even points /// - Risk-reward ratio + /// + /// # Errors + /// + /// Propagates any [`ProbabilityError`] returned by + /// [`probability_of_profit`], [`probability_of_loss`], + /// [`calculate_extreme_probabilities`] or [`expected_value`]; + /// typically [`ProbabilityError::CalculationError`] when an + /// underlying break-even or payoff evaluation fails. fn analyze_probabilities( &self, volatility_adj: Option, @@ -132,6 +140,15 @@ pub trait ProbabilityAnalysis: Strategies + Profit { /// This function relies on several auxiliary methods and traits, such as /// `get_underlying_price`, `best_range_to_show`, and `calculate_profit_at`, /// which are defined in the module's traits and utilities. + /// + /// # Errors + /// + /// Returns [`ProbabilityError::CalculationError`] when the display + /// range cannot be computed or when profit evaluation fails at a + /// sample point, and propagates any [`ProbabilityError`] surfaced by + /// [`probability_at`] (typically + /// [`ProbabilityCalculationErrorKind::InvalidProbabilityRange`] + /// for malformed volatility adjustments). fn expected_value( &self, volatility_adj: Option, @@ -211,6 +228,13 @@ pub trait ProbabilityAnalysis: Strategies + Profit { /// # Returns /// /// - `Result`: The probability of profit (between 0 and 1) or an error + /// + /// # Errors + /// + /// Returns [`ProbabilityError::RangeError`] when the profit ranges + /// cannot be constructed, or propagates + /// [`ProbabilityError::CalculationError`] from the probability + /// integration over the computed ranges. fn probability_of_profit( &self, volatility_adj: Option, @@ -248,6 +272,13 @@ pub trait ProbabilityAnalysis: Strategies + Profit { /// # Returns /// /// - `Result`: The probability of loss (between 0 and 1) or an error + /// + /// # Errors + /// + /// Returns [`ProbabilityError::RangeError`] when the loss ranges + /// cannot be constructed, or propagates + /// [`ProbabilityError::CalculationError`] from the probability + /// integration over the computed ranges. fn probability_of_loss( &self, volatility_adj: Option, @@ -286,6 +317,13 @@ pub trait ProbabilityAnalysis: Strategies + Profit { /// /// - `Result<(Positive, Positive), ProbabilityError>`: A tuple containing (probability_of_max_profit, /// probability_of_max_loss) or an error + /// + /// # Errors + /// + /// Propagates any [`ProbabilityError`] returned by + /// [`probability_of_profit`] and [`probability_of_loss`]; typically + /// [`ProbabilityError::CalculationError`] when a tail integration + /// beyond the break-even points fails. fn calculate_extreme_probabilities( &self, volatility_adj: Option, @@ -345,6 +383,13 @@ pub trait ProbabilityAnalysis: Strategies + Profit { /// # Returns /// - `Result, ProbabilityError>`: A vector of price ranges /// that result in profit, or an error + /// + /// # Errors + /// + /// Returns [`ProbabilityError::RangeError`] when no break-even + /// points can be derived from the strategy, or + /// [`ProbabilityError::CalculationError`] when the profit + /// evaluation fails at a sampled price. fn get_profit_ranges(&self) -> Result, ProbabilityError>; /// # Get Profit/Loss Ranges @@ -361,6 +406,12 @@ pub trait ProbabilityAnalysis: Strategies + Profit { /// profit/loss ranges sorted by their price boundaries. On failure, returns a /// `ProbabilityError` indicating what went wrong during the analysis. /// + /// # Errors + /// + /// Returns [`ProbabilityError::RangeError`] when no break-even + /// points can be derived from the strategy, or + /// [`ProbabilityError::CalculationError`] when the loss evaluation + /// fails at a sampled price. fn get_loss_ranges(&self) -> Result, ProbabilityError>; } diff --git a/src/strategies/protective_put.rs b/src/strategies/protective_put.rs index fab2d8af..4fb97736 100644 --- a/src/strategies/protective_put.rs +++ b/src/strategies/protective_put.rs @@ -169,6 +169,11 @@ impl ProtectivePut { } /// Calculates the net delta of the strategy. + /// + /// # Errors + /// + /// Propagates any [`GreeksError`] returned by + /// [`LegAble::delta`] on the spot leg or the long-put leg. pub fn net_delta(&self) -> Result { let spot_delta = self.spot_leg.delta()?; let put_delta = self.long_put.delta()?; @@ -176,6 +181,14 @@ impl ProtectivePut { } /// Calculates the maximum loss potential. + /// + /// # Errors + /// + /// Returns [`PricingError::MethodError`] with method + /// `protective_put` when the put strike equals or exceeds the spot + /// cost basis (the hedge fully neutralises downside and the + /// decomposition is ill-posed), or when arithmetic on `Positive` + /// operands overflows. pub fn max_loss_potential(&self) -> Result { let put_strike = self.put_strike(); let cost_basis = self.spot_leg.cost_basis; diff --git a/src/strategies/shared.rs b/src/strategies/shared.rs index 7c7e9ba9..39f81256 100644 --- a/src/strategies/shared.rs +++ b/src/strategies/shared.rs @@ -243,6 +243,14 @@ pub fn debit_spread_break_even( /// # Returns /// /// The profit ratio as a percentage, or an error if calculation fails. +/// +/// # Errors +/// +/// Currently infallible - every branch returns `Ok`, including the +/// sentinel cases where `max_loss` or `max_profit` is zero. The `Result` +/// signature is retained so future tweaks to the ratio definition (e.g. +/// checked division with rounding) can surface numerical failures without +/// breaking the public API. pub fn calculate_profit_ratio( max_profit: Positive, max_loss: Positive, From 64fd3617ff7cc96c1110a4f1b5c6f4b86cec8c38 Mon Sep 17 00:00:00 2001 From: Joaquin Bejar Date: Sun, 19 Apr 2026 11:14:48 +0200 Subject: [PATCH 04/10] M4 #334: add # Errors sections to greeks/ pub fns Added concise # Errors rustdoc sections to 16 public Result-returning functions across src/greeks/: - equations.rs: gamma, theta, vega, rho, rho_d, vanna, vomma, veta, charm, color (10 analytical Greeks) - numerical.rs: numerical_delta, numerical_gamma, numerical_vega, numerical_theta, numerical_rho (5) - utils.rs: calculate_delta_neutral_sizes (1) Each section references concrete error variants (GreeksError::Pricing, ExpirationDate, DeltaNeutrality with kind) via intra-doc links. Refs #334 --- src/greeks/equations.rs | 69 +++++++++++++++++++++++++++++++++++++++++ src/greeks/numerical.rs | 33 ++++++++++++++++++++ src/greeks/utils.rs | 10 ++++++ 3 files changed, 112 insertions(+) diff --git a/src/greeks/equations.rs b/src/greeks/equations.rs index 92b2295c..bdaf0554 100644 --- a/src/greeks/equations.rs +++ b/src/greeks/equations.rs @@ -646,6 +646,14 @@ pub fn delta(option: &Options) -> Result { /// provided in consistent units. /// - If the implied volatility or time to expiration is very small, the result may be close to 0, /// as gamma becomes negligible in those cases. +/// +/// # Errors +/// +/// Returns [`GreeksError::ExpirationDate`] when the option's expiration +/// cannot be converted to a positive year fraction, and propagates any +/// [`GreeksError`] surfaced by [`numerical_gamma`] for non-European +/// options (typically [`GreeksError::Pricing`] when the perturbation +/// evaluation fails). pub fn gamma(option: &Options) -> Result { if !matches!(option.option_type, OptionType::European) { return crate::greeks::numerical::numerical_gamma(option); @@ -780,6 +788,13 @@ pub fn gamma(option: &Options) -> Result { /// - A positive Theta means the option gains value as time passes (rare and usually for short positions). /// - A negative Theta is typical for long positions, as the option loses extrinsic value over time. /// - If the implied volatility is zero, Theta may be close to zero for far-out-of-the-money options. +/// +/// # Errors +/// +/// Returns [`GreeksError::ExpirationDate`] when the option's expiration +/// cannot be converted to a positive year fraction, and propagates any +/// [`GreeksError`] surfaced by [`numerical_theta`] for non-European +/// options. pub fn theta(option: &Options) -> Result { let t = option.expiration_date.get_years()?; if t == Decimal::ZERO { @@ -912,6 +927,13 @@ pub fn theta(option: &Options) -> Result { /// in-the-money or out-of-the-money. /// - For shorter time to expiration, Vega is smaller as the sensitivity to volatility diminishes. /// - A positive Vega indicates that an increase in implied volatility will increase the option's value. +/// +/// # Errors +/// +/// Returns [`GreeksError::ExpirationDate`] when the option's expiration +/// cannot be converted to a positive year fraction, and propagates any +/// [`GreeksError`] surfaced by [`numerical_vega`] for non-European +/// options. pub fn vega(option: &Options) -> Result { let expiration_date: Positive = option.expiration_date.get_years()?; if expiration_date == Decimal::ZERO { @@ -1036,6 +1058,13 @@ pub fn vega(option: &Options) -> Result { /// sensitive to changes in the risk-free rate. /// - Call options have positive rho values, as an increase in interest rates increases their value. /// - Put options have negative rho values, as an increase in interest rates decreases their value. +/// +/// # Errors +/// +/// Returns [`GreeksError::ExpirationDate`] when the option's expiration +/// cannot be converted to a positive year fraction, and propagates any +/// [`GreeksError`] surfaced by [`numerical_rho`] for non-European +/// options. pub fn rho(option: &Options) -> Result { // Get time to expiration first and validate let t = option.expiration_date.get_years()?; @@ -1171,6 +1200,13 @@ pub fn rho(option: &Options) -> Result { /// leading to a positive dividend sensitivity. /// - This calculation assumes that dividends are continuously compounded at the dividend yield rate. /// - \( Rho_d \) is generally more significant for options with longer times to expiration. +/// +/// # Errors +/// +/// Returns [`GreeksError::ExpirationDate`] when the option's expiration +/// cannot be converted to a positive year fraction, and propagates any +/// [`GreeksError`] surfaced by intermediate Black–Scholes kernels +/// (typically [`GreeksError::Pricing`] on numerical failure). pub fn rho_d(option: &Options) -> Result { let expiration_date: Positive = option.expiration_date.get_years()?; let d1 = d1( @@ -1295,6 +1331,13 @@ pub fn alpha(option: &Options) -> Result { /// /// - This function assumes that the dividend yield \(q\) and the time to expiration \(T\) are /// provided in consistent units. +/// +/// # Errors +/// +/// Returns [`GreeksError::ExpirationDate`] when the option's expiration +/// cannot be converted to a positive year fraction, and propagates any +/// [`GreeksError`] surfaced by the underlying Black–Scholes +/// evaluation (typically [`GreeksError::Pricing`]). pub fn vanna(option: &Options) -> Result { if option.implied_volatility == ZERO { return Ok(Decimal::ZERO); @@ -1413,6 +1456,13 @@ pub fn vanna(option: &Options) -> Result { /// less and less. /// If you think the implied volatility will be volatile in the short term /// you should typically try to find options with high Vomma. +/// +/// # Errors +/// +/// Returns [`GreeksError::ExpirationDate`] when the option's expiration +/// cannot be converted to a positive year fraction, and propagates any +/// [`GreeksError`] surfaced by the underlying Black–Scholes +/// evaluation. pub fn vomma(option: &Options) -> Result { let expiration_date: Positive = option.expiration_date.get_years()?; if expiration_date == Decimal::ZERO { @@ -1520,6 +1570,13 @@ pub fn vomma(option: &Options) -> Result { /// - It is common practice to divide the mathematical result of veta by 100 times /// the number of days per year to reduce the value to the percentage change in /// vega per one day. +/// +/// # Errors +/// +/// Returns [`GreeksError::ExpirationDate`] when the option's expiration +/// cannot be converted to a positive year fraction, and propagates any +/// [`GreeksError`] surfaced by the underlying Black–Scholes +/// evaluation. pub fn veta(option: &Options) -> Result { let expiration_date: Positive = option.expiration_date.get_years()?; if expiration_date == Decimal::ZERO { @@ -1669,6 +1726,12 @@ pub fn veta(option: &Options) -> Result { /// /// - With zero DTE Charm can be considered as zero. /// - Charm effects are more pronounced near expiration. +/// +/// # Errors +/// +/// Returns [`GreeksError::ExpirationDate`] when the option's expiration +/// cannot be converted to a positive year fraction, and propagates any +/// [`GreeksError`] surfaced by intermediate Black–Scholes kernels. pub fn charm(option: &Options) -> Result { let tau = option.expiration_date.get_years()?; // if DTE is zero we can assume Charm is also zero @@ -1803,6 +1866,12 @@ pub fn charm(option: &Options) -> Result { /// - Color will be more pronounced as expiration date approaches. /// - When volatility increases Color sensitivity decrease. /// - Deep ITM and OTM options have negligible Color. +/// +/// # Errors +/// +/// Returns [`GreeksError::ExpirationDate`] when the option's expiration +/// cannot be converted to a positive year fraction, and propagates any +/// [`GreeksError`] surfaced by intermediate Black–Scholes kernels. pub fn color(option: &Options) -> Result { let tau = option.expiration_date.get_years()?; // if DTE is zero we can assume Color is also zero diff --git a/src/greeks/numerical.rs b/src/greeks/numerical.rs index c61fa8a2..cc6f2da7 100644 --- a/src/greeks/numerical.rs +++ b/src/greeks/numerical.rs @@ -22,6 +22,14 @@ const H: Decimal = dec!(0.01); /// /// Delta measures the rate of change of the option price with respect to /// changes in the underlying asset's price. +/// +/// # Errors +/// +/// Propagates any [`PricingError`] returned by the unified-pricing +/// evaluator on the perturbed option clones, wrapped as +/// [`GreeksError::Pricing`]; typically +/// [`PricingError::ExpirationDate`] or [`PricingError::MethodError`] on +/// numerical failure. pub fn numerical_delta(option: &Options) -> Result { let mut opt_plus = option.clone(); opt_plus.underlying_price = @@ -41,6 +49,12 @@ pub fn numerical_delta(option: &Options) -> Result { /// /// Gamma measures the rate of change of delta with respect to changes in the /// underlying asset's price. +/// +/// # Errors +/// +/// Propagates any [`PricingError`] returned by the unified-pricing +/// evaluator on the three perturbed option clones, wrapped as +/// [`GreeksError::Pricing`]. pub fn numerical_gamma(option: &Options) -> Result { let mut opt_plus = option.clone(); opt_plus.underlying_price = @@ -61,6 +75,12 @@ pub fn numerical_gamma(option: &Options) -> Result { /// /// Vega measures the sensitivity of the option price to changes in the /// underlying asset's volatility. +/// +/// # Errors +/// +/// Propagates any [`PricingError`] returned by the unified-pricing +/// evaluator on the perturbed option clones, wrapped as +/// [`GreeksError::Pricing`]. pub fn numerical_vega(option: &Options) -> Result { let mut opt_plus = option.clone(); opt_plus.implied_volatility = @@ -79,6 +99,13 @@ pub fn numerical_vega(option: &Options) -> Result { /// Calculates theta numerically using finite differences. /// /// Theta measures the rate of decay of the option's value over time. +/// +/// # Errors +/// +/// Returns [`GreeksError::ExpirationDate`] when the option's expiration +/// cannot be resolved, and propagates any [`PricingError`] returned by +/// the unified-pricing evaluator on the perturbed option clones +/// (wrapped as [`GreeksError::Pricing`]). pub fn numerical_theta(option: &Options) -> Result { let t = option.expiration_date.get_years()?; if t < H { @@ -97,6 +124,12 @@ pub fn numerical_theta(option: &Options) -> Result { /// /// Rho measures the sensitivity of the option price to changes in the /// risk-free interest rate. +/// +/// # Errors +/// +/// Propagates any [`PricingError`] returned by the unified-pricing +/// evaluator on the perturbed option clones, wrapped as +/// [`GreeksError::Pricing`]. pub fn numerical_rho(option: &Options) -> Result { let mut opt_plus = option.clone(); opt_plus.risk_free_rate += H; diff --git a/src/greeks/utils.rs b/src/greeks/utils.rs index 860cd944..95ad99ab 100644 --- a/src/greeks/utils.rs +++ b/src/greeks/utils.rs @@ -469,6 +469,16 @@ pub(crate) fn calculate_d_values(option: &Options) -> Result<(Decimal, Decimal), /// # Ok(()) /// # } /// ``` +/// +/// # Errors +/// +/// Returns [`GreeksError::DeltaNeutrality`] with +/// [`DeltaNeutralityErrorKind::ZeroDelta`] when both deltas are zero, +/// [`DeltaNeutralityErrorKind::EqualDeltas`] when `delta1 == delta2`, +/// [`DeltaNeutralityErrorKind::SameSignDeltas`] when the two deltas +/// share the same sign (no neutral mix exists), or +/// [`DeltaNeutralityErrorKind::NegativePositionSize`] when the derived +/// sizes fall below zero for the provided `total_size`. pub fn calculate_delta_neutral_sizes( delta1: Decimal, delta2: Decimal, From 49dbf6e816424ffff7052e5618c13dde9b955a14 Mon Sep 17 00:00:00 2001 From: Joaquin Bejar Date: Sun, 19 Apr 2026 11:16:21 +0200 Subject: [PATCH 05/10] M4 #334: add # Errors sections to volatility/ pub fns Added concise # Errors rustdoc sections to 11 public Result-returning functions across src/volatility/: - traits.rs: AtmIvProvider::atm_iv (1) - utils.rs: constant_volatility, historical_volatility, ewma_volatility, implied_volatility, garch_volatility, simulate_heston_volatility, uncertain_volatility_bounds, annualized_volatility, de_annualized_volatility, adjust_volatility (10) Each section references concrete VolatilityError variants (AtmIvUnavailable, IvNotFound, NoConvergence, NumericalFailure, PositiveError, InvalidTime, Options) via intra-doc links. Refs #334 --- src/volatility/traits.rs | 9 +++++ src/volatility/utils.rs | 71 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/src/volatility/traits.rs b/src/volatility/traits.rs index 6a963581..1a8141a7 100644 --- a/src/volatility/traits.rs +++ b/src/volatility/traits.rs @@ -96,6 +96,15 @@ pub trait AtmIvProvider { /// * `Ok(Some(Positive))` - If the ATM implied volatility is successfully retrieved. /// * `Ok(None)` - If the ATM implied volatility is not available or not applicable. /// * `Err(Box)` - If an error occurs during the retrieval process. + /// + /// # Errors + /// + /// Returns [`VolatilityError::AtmIvUnavailable`] with a wrapped + /// [`VolatilityError::Chain`] source when the underlying container + /// (typically an option chain) cannot supply an ATM implied + /// volatility for the current observation, or + /// [`VolatilityError::IvNotFound`] when no matching strike produced + /// a valid candidate. fn atm_iv(&self) -> Result<&Positive, VolatilityError>; } diff --git a/src/volatility/utils.rs b/src/volatility/utils.rs index f085329b..bb11dce7 100644 --- a/src/volatility/utils.rs +++ b/src/volatility/utils.rs @@ -26,6 +26,13 @@ use positive::pos_or_panic; /// # Returns /// /// The calculated volatility as a Decimal. +/// +/// # Errors +/// +/// Returns [`VolatilityError::NumericalFailure`] when the length of +/// `returns` cannot be represented as a `Decimal` or when the +/// variance computation overflows, and [`VolatilityError::PositiveError`] +/// if the final square-root produces a non-positive candidate. pub fn constant_volatility(returns: &[Decimal]) -> Result { let n_dec = Decimal::from_usize(returns.len()).ok_or_else(|| VolatilityError::NumericalFailure { @@ -62,6 +69,13 @@ pub fn constant_volatility(returns: &[Decimal]) -> Result Date: Sun, 19 Apr 2026 11:19:44 +0200 Subject: [PATCH 06/10] M4 #334: add # Errors sections to pricing/ and metrics/ pub fns Added concise # Errors rustdoc sections to 28 public Result-returning functions across src/pricing/ and src/metrics/: - pricing/american.rs: barone_adesi_whaley (1) - pricing/binomial_model.rs: price_binomial, generate_binomial_tree (2) - pricing/barrier.rs, binary.rs, lookback.rs, compound.rs, chooser.rs, cliquet.rs: *_black_scholes for each exotic (6) - pricing/black_scholes_model.rs: black_scholes, BlackScholes trait get_option + calculate_price_black_scholes (3) - pricing/monte_carlo.rs: monte_carlo_option_pricing (1) - pricing/payoff.rs: Profit::calculate_profit_at, get_point_at_price (2) - pricing/telegraph.rs: telegraph (1) - pricing/utils.rs: simulate_returns (1) - pricing/unified.rs: price_option, Priceable::price (2) - metrics/price/{put_call_ratio,strike_concentration,volatility_skew}.rs: one trait method each (3) - metrics/stress/{price_shock,time_decay,volatility_sensitivity}.rs: one *_surface trait method each (3) - metrics/temporal/{charm,color,theta}.rs: one *_surface trait method each (3) Each section references concrete error variants (PricingError, SurfaceError, CurveError, DecimalError) via intra-doc links. Refs #334 --- src/metrics/price/put_call_ratio.rs | 7 +++++++ src/metrics/price/strike_concentration.rs | 7 +++++++ src/metrics/price/volatility_skew.rs | 7 +++++++ src/metrics/stress/price_shock.rs | 8 ++++++++ src/metrics/stress/time_decay.rs | 8 ++++++++ src/metrics/stress/volatility_sensitivity.rs | 8 ++++++++ src/metrics/temporal/charm.rs | 7 +++++++ src/metrics/temporal/color.rs | 7 +++++++ src/metrics/temporal/theta.rs | 7 +++++++ src/pricing/american.rs | 9 +++++++++ src/pricing/barrier.rs | 8 ++++++++ src/pricing/binary.rs | 8 ++++++++ src/pricing/binomial_model.rs | 17 ++++++++++++++++ src/pricing/black_scholes_model.rs | 21 ++++++++++++++++++++ src/pricing/chooser.rs | 7 +++++++ src/pricing/cliquet.rs | 7 +++++++ src/pricing/compound.rs | 7 +++++++ src/pricing/lookback.rs | 7 +++++++ src/pricing/monte_carlo.rs | 9 +++++++++ src/pricing/payoff.rs | 12 +++++++++++ src/pricing/telegraph.rs | 8 ++++++++ src/pricing/unified.rs | 14 +++++++++++++ src/pricing/utils.rs | 7 +++++++ 23 files changed, 207 insertions(+) diff --git a/src/metrics/price/put_call_ratio.rs b/src/metrics/price/put_call_ratio.rs index d53427ec..bcf464b4 100644 --- a/src/metrics/price/put_call_ratio.rs +++ b/src/metrics/price/put_call_ratio.rs @@ -125,6 +125,13 @@ pub trait PutCallRatioCurve { /// # Note /// - The `Curve` returned should ideally conform to the constraints and /// ordering requirements specified in the `Curve` documentation. + /// + /// # Errors + /// + /// Returns [`CurveError::ConstructionError`] when the premium data + /// cannot be aggregated into a monotonic curve (e.g. empty chain or + /// non-finite premium values), and may propagate + /// [`CurveError::InterpolationError`] from downstream smoothing. fn premium_weighted_pcr(&self) -> Result; } diff --git a/src/metrics/price/strike_concentration.rs b/src/metrics/price/strike_concentration.rs index a5ffa8ee..9cf26581 100644 --- a/src/metrics/price/strike_concentration.rs +++ b/src/metrics/price/strike_concentration.rs @@ -108,6 +108,13 @@ pub trait StrikeConcentrationCurve { /// # Note /// - The `Curve` returned should ideally conform to the constraints and /// ordering requirements specified in the `Curve` documentation. + /// + /// # Errors + /// + /// Returns [`CurveError::ConstructionError`] when the per-strike + /// concentration cannot be computed (empty chain, invalid premium + /// weights, or denominator underflow), and may propagate + /// [`CurveError::InterpolationError`] from downstream smoothing. fn premium_concentration(&self) -> Result; } diff --git a/src/metrics/price/volatility_skew.rs b/src/metrics/price/volatility_skew.rs index 3ca63ab5..69482c03 100644 --- a/src/metrics/price/volatility_skew.rs +++ b/src/metrics/price/volatility_skew.rs @@ -127,6 +127,13 @@ pub trait VolatilitySkewCurve { /// # Note /// - The `Curve` returned should ideally conform to the constraints and /// ordering requirements specified in the `Curve` documentation. + /// + /// # Errors + /// + /// Returns [`CurveError::ConstructionError`] when no strikes in the + /// chain carry a valid implied volatility, and may propagate + /// [`CurveError::InterpolationError`] when the smoothing stage fails + /// on the raw skew samples. fn volatility_skew(&self) -> Result; } diff --git a/src/metrics/stress/price_shock.rs b/src/metrics/stress/price_shock.rs index 4961ef37..6044804d 100644 --- a/src/metrics/stress/price_shock.rs +++ b/src/metrics/stress/price_shock.rs @@ -135,6 +135,14 @@ pub trait PriceShockSurface { /// - `Ok(Surface)`: The shock surface with price on x-axis, /// volatility on y-axis, and option value on z-axis /// - `Err(SurfaceError)`: If the surface cannot be computed + /// + /// # Errors + /// + /// Returns [`SurfaceError::ConstructionError`] when the price or + /// volatility ranges yield an empty sampling grid, and propagates + /// any [`SurfaceError::Point3DError`] or + /// [`SurfaceError::OperationError`] raised while evaluating the + /// option at each `(price, volatility)` sample. fn price_shock_surface( &self, price_range: (Positive, Positive), diff --git a/src/metrics/stress/time_decay.rs b/src/metrics/stress/time_decay.rs index eac0918d..1c2f34fa 100644 --- a/src/metrics/stress/time_decay.rs +++ b/src/metrics/stress/time_decay.rs @@ -129,6 +129,14 @@ pub trait TimeDecaySurface { /// - `Ok(Surface)`: The decay surface with price on x-axis, /// days on y-axis, and option value on z-axis /// - `Err(SurfaceError)`: If the surface cannot be computed + /// + /// # Errors + /// + /// Returns [`SurfaceError::ConstructionError`] when the sampling + /// grid is empty or when all samples fail price evaluation, and + /// propagates [`SurfaceError::Point3DError`] or + /// [`SurfaceError::OperationError`] from the underlying Black–Scholes + /// evaluator at each `(price, days)` sample. fn time_decay_surface( &self, price_range: (Positive, Positive), diff --git a/src/metrics/stress/volatility_sensitivity.rs b/src/metrics/stress/volatility_sensitivity.rs index e947bbb0..2afaeadd 100644 --- a/src/metrics/stress/volatility_sensitivity.rs +++ b/src/metrics/stress/volatility_sensitivity.rs @@ -133,6 +133,14 @@ pub trait VolatilitySensitivitySurface { /// - `Ok(Surface)`: The sensitivity surface with price on x-axis, /// volatility on y-axis, and option value on z-axis /// - `Err(SurfaceError)`: If the surface cannot be computed + /// + /// # Errors + /// + /// Returns [`SurfaceError::ConstructionError`] when the price or + /// volatility ranges yield an empty sampling grid, and propagates + /// [`SurfaceError::Point3DError`] or + /// [`SurfaceError::OperationError`] from the underlying Black–Scholes + /// evaluator at each `(price, volatility)` sample. fn volatility_sensitivity_surface( &self, price_range: (Positive, Positive), diff --git a/src/metrics/temporal/charm.rs b/src/metrics/temporal/charm.rs index 42b5fa7f..7bd099ce 100644 --- a/src/metrics/temporal/charm.rs +++ b/src/metrics/temporal/charm.rs @@ -124,6 +124,13 @@ pub trait CharmSurface { /// - `Ok(Surface)`: The charm surface with price on x-axis, /// days on y-axis, and charm on z-axis /// - `Err(SurfaceError)`: If the surface cannot be computed + /// + /// # Errors + /// + /// Returns [`SurfaceError::ConstructionError`] when the sampling + /// grid is empty, and propagates [`SurfaceError::Point3DError`] or + /// [`SurfaceError::OperationError`] from the intermediate + /// [`charm`] evaluation at each `(price, days)` sample. fn charm_surface( &self, price_range: (Positive, Positive), diff --git a/src/metrics/temporal/color.rs b/src/metrics/temporal/color.rs index a6acd445..27ea9442 100644 --- a/src/metrics/temporal/color.rs +++ b/src/metrics/temporal/color.rs @@ -124,6 +124,13 @@ pub trait ColorSurface { /// - `Ok(Surface)`: The color surface with price on x-axis, /// days on y-axis, and color on z-axis /// - `Err(SurfaceError)`: If the surface cannot be computed + /// + /// # Errors + /// + /// Returns [`SurfaceError::ConstructionError`] when the sampling + /// grid is empty, and propagates [`SurfaceError::Point3DError`] or + /// [`SurfaceError::OperationError`] from the intermediate + /// [`color`] evaluation at each `(price, days)` sample. fn color_surface( &self, price_range: (Positive, Positive), diff --git a/src/metrics/temporal/theta.rs b/src/metrics/temporal/theta.rs index d8438dd3..8e492904 100644 --- a/src/metrics/temporal/theta.rs +++ b/src/metrics/temporal/theta.rs @@ -134,6 +134,13 @@ pub trait ThetaSurface { /// - `Ok(Surface)`: The theta surface with price on x-axis, /// days on y-axis, and theta on z-axis /// - `Err(SurfaceError)`: If the surface cannot be computed + /// + /// # Errors + /// + /// Returns [`SurfaceError::ConstructionError`] when the sampling + /// grid is empty, and propagates [`SurfaceError::Point3DError`] or + /// [`SurfaceError::OperationError`] from the intermediate + /// [`theta`] evaluation at each `(price, days)` sample. fn theta_surface( &self, price_range: (Positive, Positive), diff --git a/src/pricing/american.rs b/src/pricing/american.rs index 5fbdfa27..4e3d41dc 100644 --- a/src/pricing/american.rs +++ b/src/pricing/american.rs @@ -119,6 +119,15 @@ const TOLERANCE: f64 = 1e-6; /// # Ok(()) /// # } /// ``` +/// +/// # Errors +/// +/// Returns [`PricingError::MethodError`] (with method +/// `barone_adesi_whaley`) when the intermediate Newton–Raphson +/// iteration cannot converge on the early-exercise boundary +/// [`Sx`], and [`PricingError::SqrtFailure`] when the quadratic +/// formula receives a negative discriminant from the approximation +/// parameters. pub fn barone_adesi_whaley( spot: Positive, strike: Positive, diff --git a/src/pricing/barrier.rs b/src/pricing/barrier.rs index 1d77011c..9764feb0 100644 --- a/src/pricing/barrier.rs +++ b/src/pricing/barrier.rs @@ -14,6 +14,14 @@ use rust_decimal_macros::dec; /// Prices a barrier option using the Black-Scholes analytical extension. /// Supports Down-And-In, Up-And-In, Down-And-Out, and Up-And-Out variants. +/// +/// # Errors +/// +/// Returns [`PricingError::UnsupportedOptionType`] when `option` +/// is not a [`OptionType::Barrier`] variant, and propagates any +/// [`PricingError`] raised by the underlying Black\u2013Scholes closed +/// form on the decomposed vanilla components (typically +/// [`PricingError::ExpirationDate`] or [`PricingError::Positive`]). pub fn barrier_black_scholes(option: &Options) -> Result { let (barrier_type, barrier_level, rebate) = match &option.option_type { OptionType::Barrier { diff --git a/src/pricing/binary.rs b/src/pricing/binary.rs index 651dfdef..7684542d 100644 --- a/src/pricing/binary.rs +++ b/src/pricing/binary.rs @@ -49,6 +49,14 @@ const DEFAULT_CASH_PAYOUT: Decimal = dec!(1.0); /// # Returns /// /// The option price as a `Decimal`, or a `PricingError` if pricing fails. +/// +/// # Errors +/// +/// Returns [`PricingError::UnsupportedOptionType`] when `option` is +/// not an [`OptionType::Binary`] variant, and propagates any +/// [`PricingError`] raised by intermediate Black–Scholes kernels +/// (typically [`PricingError::ExpirationDate`] or +/// [`PricingError::Positive`]). pub fn binary_black_scholes(option: &Options) -> Result { match &option.option_type { OptionType::Binary { binary_type } => match binary_type { diff --git a/src/pricing/binomial_model.rs b/src/pricing/binomial_model.rs index 2e7e2e4b..30e41f51 100644 --- a/src/pricing/binomial_model.rs +++ b/src/pricing/binomial_model.rs @@ -91,6 +91,14 @@ pub struct BinomialPricingParams<'a> { /// - The model's accuracy increases with the number of steps, but so does the computation time. /// - This model assumes that the underlying asset follows a multiplicative binomial process. /// - For American options, this model accounts for the possibility of early exercise. +/// +/// # Errors +/// +/// Returns [`PricingError::SqrtFailure`] when the up-factor exponent +/// produces an invalid `Decimal`, [`PricingError::BinomialNodeMissing`] +/// when the induction step cannot read an intermediate node, and +/// [`PricingError::Positive`] when any `Positive` construction +/// downstream (e.g. strike × discount factor) underflows below zero. pub fn price_binomial(params: BinomialPricingParams) -> Result { let mut info = PayoffInfo { spot: params.asset, @@ -211,6 +219,15 @@ pub fn price_binomial(params: BinomialPricingParams) -> Result BinomialTreeResult { let mut info = PayoffInfo { spot: params.asset, diff --git a/src/pricing/black_scholes_model.rs b/src/pricing/black_scholes_model.rs index 817bfaaf..8e1f93e5 100644 --- a/src/pricing/black_scholes_model.rs +++ b/src/pricing/black_scholes_model.rs @@ -57,6 +57,14 @@ use tracing::trace; /// - `d1 = (ln(S/K) + (r + σ²/2) * T) / (σ * √T)` /// - `d2 = d1 - σ * √T` /// +/// # Errors +/// +/// Returns [`PricingError::ExpirationDate`] when the expiration +/// cannot be converted to a positive year fraction, +/// [`PricingError::MethodError`] when the `d1`/`d2` evaluation hits a +/// numerical wall (e.g. zero volatility or non-finite intermediate +/// value), and [`PricingError::UnsupportedOptionType`] for exotic +/// option types not handled by the closed form. pub fn black_scholes(option: &Options) -> Result { let (d1, d2, expiry_time) = calculate_d1_d2_and_time(option)?; match option.option_type { @@ -282,6 +290,12 @@ pub trait BlackScholes { /// /// * `Result<&Options, PricingError>` - A reference to the Options struct on success, /// or an error if the option data cannot be retrieved. + /// + /// # Errors + /// + /// Returns [`PricingError::MethodError`] when the implementor + /// cannot resolve the current option (e.g. a placeholder wrapper + /// before the option has been bound to a trade or position). fn get_option(&self) -> Result<&Options, PricingError>; /// Calculates the price of the option using the Black-Scholes model. @@ -293,6 +307,13 @@ pub trait BlackScholes { /// /// * `Result` - The calculated option price as a Decimal /// on success, or an error if the calculation fails. + /// + /// # Errors + /// + /// Propagates any [`PricingError`] returned by + /// [`BlackScholesOption::get_option`] or by [`black_scholes`] + /// (typically [`PricingError::ExpirationDate`] or + /// [`PricingError::MethodError`]). fn calculate_price_black_scholes(&self) -> Result { let option = self.get_option()?; black_scholes(option) diff --git a/src/pricing/chooser.rs b/src/pricing/chooser.rs index a587900f..00397ee6 100644 --- a/src/pricing/chooser.rs +++ b/src/pricing/chooser.rs @@ -41,6 +41,13 @@ use rust_decimal_macros::dec; /// # Returns /// /// The option price as a `Decimal`, or a `PricingError` if pricing fails. +/// +/// # Errors +/// +/// Returns [`PricingError::UnsupportedOptionType`] when `option` is +/// not an [`OptionType::Chooser`] variant, and propagates any +/// [`PricingError`] raised by the Black–Scholes evaluation at the +/// chooser date (call and put side). pub fn chooser_black_scholes(option: &Options) -> Result { match &option.option_type { OptionType::Chooser { choice_date } => simple_chooser_price(option, *choice_date), diff --git a/src/pricing/cliquet.rs b/src/pricing/cliquet.rs index f99a2fef..a508c926 100644 --- a/src/pricing/cliquet.rs +++ b/src/pricing/cliquet.rs @@ -39,6 +39,13 @@ use rust_decimal_macros::dec; /// # Returns /// /// The option price as a `Decimal`, or a `PricingError` if pricing fails. +/// +/// # Errors +/// +/// Returns [`PricingError::UnsupportedOptionType`] when `option` is +/// not an [`OptionType::Cliquet`] variant, and propagates any +/// [`PricingError`] raised by intermediate Black–Scholes kernels on +/// the per-reset forward components. pub fn cliquet_black_scholes(option: &Options) -> Result { match &option.option_type { OptionType::Cliquet { reset_dates } => price_cliquet(option, reset_dates), diff --git a/src/pricing/compound.rs b/src/pricing/compound.rs index 81f8fb60..86636633 100644 --- a/src/pricing/compound.rs +++ b/src/pricing/compound.rs @@ -207,6 +207,13 @@ fn standard_normal_cdf(x: f64) -> f64 { /// # Returns /// /// The option price as a `Decimal`, or a `PricingError` if pricing fails. +/// +/// # Errors +/// +/// Returns [`PricingError::UnsupportedOptionType`] when `option` is +/// not an [`OptionType::Compound`] variant, and propagates any +/// [`PricingError`] raised by the Black–Scholes evaluation of the +/// outer option on the inner-option implied value. pub fn compound_black_scholes(option: &Options) -> Result { match &option.option_type { OptionType::Compound { underlying_option } => price_compound(option, underlying_option), diff --git a/src/pricing/lookback.rs b/src/pricing/lookback.rs index 431c483e..3c0e229e 100644 --- a/src/pricing/lookback.rs +++ b/src/pricing/lookback.rs @@ -42,6 +42,13 @@ use rust_decimal_macros::dec; /// # Returns /// /// The option price as a `Decimal`, or a `PricingError` if pricing fails. +/// +/// # Errors +/// +/// Returns [`PricingError::UnsupportedOptionType`] when `option` is +/// not an [`OptionType::Lookback`] variant, and propagates any +/// [`PricingError`] raised by intermediate Black–Scholes kernels +/// on the running extremum decomposition. pub fn lookback_black_scholes(option: &Options) -> Result { match &option.option_type { OptionType::Lookback { lookback_type } => match lookback_type { diff --git a/src/pricing/monte_carlo.rs b/src/pricing/monte_carlo.rs index e25ca554..43857780 100644 --- a/src/pricing/monte_carlo.rs +++ b/src/pricing/monte_carlo.rs @@ -32,6 +32,15 @@ use rust_decimal::{Decimal, MathematicalOps}; /// - Calculate the payoff of the option for this simulation (for a call option, this is `max(st - strike_price, 0)`). /// - Add the payoff to the `payoff_sum`. /// 4. Return the average payoff discounted to its present value. +/// +/// # Errors +/// +/// Returns [`PricingError::ExpirationDate`] when the option's +/// expiration cannot be converted to a positive year fraction, and +/// [`PricingError::MethodError`] when the GBM discretisation +/// encounters a non-finite value (e.g. volatility overflow) or when +/// the terminal payoff averaging produces a non-representable +/// `Decimal`. pub fn monte_carlo_option_pricing( option: &Options, steps: usize, // Number of time steps diff --git a/src/pricing/payoff.rs b/src/pricing/payoff.rs index 242ddce4..2fdf86c4 100644 --- a/src/pricing/payoff.rs +++ b/src/pricing/payoff.rs @@ -226,6 +226,13 @@ pub trait Profit { /// /// * `Result` - The calculated profit as a Decimal value, /// or an error if the calculation fails + /// + /// # Errors + /// + /// Returns [`PricingError::MethodError`] when the strategy cannot + /// evaluate its payoff at `price` (typically propagated from the + /// underlying [`Position::pnl_at_expiration`] or Black–Scholes + /// evaluation). fn calculate_profit_at(&self, price: &Positive) -> Result; /// Creates a chart point representation of the profit at the given price. @@ -241,6 +248,11 @@ pub trait Profit { /// /// * `ChartPoint<(f64, f64)>` - A formatted chart point with coordinates (price, profit), /// styling, and a formatted profit label + /// + /// # Errors + /// + /// Propagates any [`PricingError`] returned by + /// [`Profit::calculate_profit_at`]. fn get_point_at_price(&self, _price: &Positive) -> Result<(Decimal, Decimal), PricingError> { let profit = self.calculate_profit_at(_price)?; let price: Decimal = _price.into(); diff --git a/src/pricing/telegraph.rs b/src/pricing/telegraph.rs index 06becd2a..5c2a7ba0 100644 --- a/src/pricing/telegraph.rs +++ b/src/pricing/telegraph.rs @@ -316,6 +316,14 @@ pub(crate) fn estimate_telegraph_parameters( /// The function handles parameter estimation automatically if transition rates are not provided. /// When missing, it simulates returns based on the option's implied volatility to estimate /// appropriate telegraph parameters. +/// +/// # Errors +/// +/// Returns [`PricingError::ExpirationDate`] when the option's +/// expiration cannot be converted, [`PricingError::MethodError`] +/// when the finite-difference recurrence fails to populate a node +/// (e.g. parameter estimation produces degenerate rates) or when the +/// terminal averaging yields a non-finite value. pub fn telegraph( option: &Options, no_steps: usize, diff --git a/src/pricing/unified.rs b/src/pricing/unified.rs index d8e06211..70c0cdaf 100644 --- a/src/pricing/unified.rs +++ b/src/pricing/unified.rs @@ -68,6 +68,15 @@ pub enum PricingEngine { /// let price = price_option(&option, &engine)?; /// Ok::<(), optionstratlib::error::PricingError>(()) /// ``` +/// +/// # Errors +/// +/// Propagates any [`PricingError`] returned by the selected engine: +/// [`PricingError::ExpirationDate`] or [`PricingError::MethodError`] +/// from Black–Scholes, [`PricingError::BinomialNodeMissing`] or +/// [`PricingError::SqrtFailure`] from the binomial lattice, or the +/// equivalent failures from exotic engines (barrier, binary, +/// compound, chooser, cliquet, lookback, telegraph, Monte Carlo). pub fn price_option(option: &Options, engine: &PricingEngine) -> PricingResult { match engine { PricingEngine::ClosedFormBS => { @@ -96,6 +105,11 @@ pub trait Priceable { /// # Returns /// /// Returns the price as a `Positive` value, or a `PricingError` if pricing fails. + /// + /// # Errors + /// + /// Propagates any [`PricingError`] returned by the selected + /// engine; see [`price_option`] for the full variant breakdown. fn price(&self, engine: &PricingEngine) -> PricingResult; } diff --git a/src/pricing/utils.rs b/src/pricing/utils.rs index 58fe3310..990b36c5 100644 --- a/src/pricing/utils.rs +++ b/src/pricing/utils.rs @@ -33,6 +33,13 @@ use rust_decimal_macros::dec; /// A Result containing either: /// - Ok(`Vec`): A vector of simulated returns as Decimal numbers /// - Err(DecimalError): If there's an error in decimal calculations +/// +/// # Errors +/// +/// Returns [`DecimalError::ConversionError`] when the sampled normal +/// variate cannot be represented as a `Decimal` (e.g. NaN or out-of-range +/// float), and [`DecimalError::ArithmeticError`] when the +/// `mean + std_dev * z` combination overflows the `Decimal` range. pub fn simulate_returns( mean: Decimal, std_dev: Positive, From 239cee6b86e2581c815969c3a8d3965b3e0da583 Mon Sep 17 00:00:00 2001 From: Joaquin Bejar Date: Sun, 19 Apr 2026 11:25:58 +0200 Subject: [PATCH 07/10] M4 #334: add # Errors sections to remaining subsystems Added concise # Errors rustdoc sections to 67 public Result-returning functions across src/simulation/, src/geometrics/, src/curves/, src/surfaces/, src/pnl/, src/visualization/, src/utils/: - simulation/params.rs: ystep_as_positive (1) - simulation/steps/step.rs: next, previous, get_graph_x_value, get_graph_y_value, get_positive_value (5) - simulation/steps/x.rs: days_left, next, previous (3) - simulation/steps/y.rs: positive (1) - simulation/traits.rs: brownian, geometric_brownian, log_returns, mean_reverting, jump_diffusion, garch, heston, custom, telegraph (9) - geometrics/analysis/metrics.rs: analysis_result (1) - geometrics/analysis/traits.rs: compute_basic/shape/range/trend/risk /curve_metrics (6) - geometrics/interpolation/{linear,cubic,bilinear,spline}.rs and traits.rs: 5 algorithm methods (5) - geometrics/operations/axis.rs: get_closest_point, merge_axis_interpolate (2) - geometrics/operations/traits.rs: merge, merge_with (2) - geometrics/operations/transformations.rs: translate, scale, intersect_with, derivative_at, extrema, measure_under (6) - geometrics/utils.rs: construct (1) - curves/basic.rs: curve, get_curve_strike_versus (2) - curves/traits.rs: generate_statistical_curve, generate_refined_statistical_curve, verify_curve_metrics (3) - curves/types.rs: Point2D::from_tuple (1) - surfaces/basic.rs: surface, time_surface (2) - surfaces/traits.rs: Surfacable::surface (1) - surfaces/types.rs: Point3D::{to_tuple, from_tuple, to_f64_tuple, from_f64_tuple} (4) - pnl/metrics.rs: save_pnl_metrics, load_pnl_metrics (2) - pnl/traits.rs: calculate_pnl, calculate_pnl_at_expiration, TransactionAble::{add_transaction, get_transactions} (4) - pnl/transaction.rs: Transaction::pnl (1) - utils/csv.rs: read_ohlcv_from_zip_async (1) - utils/file.rs: prepare_file_path (1) - visualization/plotly.rs: show, render, to_interactive_html (3) Each section references concrete error variants via intra-doc links. Phase A complete: cargo clippy --lib -- -W missing_errors_doc now reports 0 warnings. Refs #334 --- src/curves/basic.rs | 15 +++++ src/curves/traits.rs | 22 ++++++ src/curves/types.rs | 8 +++ src/geometrics/analysis/metrics.rs | 7 ++ src/geometrics/analysis/traits.rs | 38 +++++++++++ src/geometrics/interpolation/bilinear.rs | 8 +++ src/geometrics/interpolation/cubic.rs | 9 +++ src/geometrics/interpolation/linear.rs | 8 +++ src/geometrics/interpolation/spline.rs | 9 +++ src/geometrics/interpolation/traits.rs | 8 +++ src/geometrics/operations/axis.rs | 12 ++++ src/geometrics/operations/traits.rs | 11 +++ src/geometrics/operations/transformations.rs | 39 +++++++++++ src/geometrics/utils.rs | 7 ++ src/pnl/metrics.rs | 13 ++++ src/pnl/traits.rs | 26 +++++++ src/pnl/transaction.rs | 8 +++ src/simulation/params.rs | 7 ++ src/simulation/steps/step.rs | 35 ++++++++++ src/simulation/steps/x.rs | 19 ++++++ src/simulation/steps/y.rs | 6 ++ src/simulation/traits.rs | 71 ++++++++++++++++++++ src/surfaces/basic.rs | 16 +++++ src/surfaces/traits.rs | 8 +++ src/surfaces/types.rs | 27 ++++++++ src/utils/csv.rs | 7 ++ src/utils/file.rs | 7 ++ src/visualization/plotly.rs | 22 ++++++ 28 files changed, 473 insertions(+) diff --git a/src/curves/basic.rs b/src/curves/basic.rs index 8f91de4a..c37464cd 100644 --- a/src/curves/basic.rs +++ b/src/curves/basic.rs @@ -41,6 +41,14 @@ pub trait BasicCurves { /// /// * `Result` - A curve object containing the plotted data points, /// or an error if the curve could not be generated + /// + /// # Errors + /// + /// Returns [`CurveError::ConstructionError`] when no strikes + /// produced valid samples for the requested axis, and propagates + /// any [`CurveError::GreeksError`] or + /// [`CurveError::InterpolationError`] surfaced by the per-strike + /// evaluator. fn curve( &self, axis: &BasicAxisTypes, @@ -64,6 +72,13 @@ pub trait BasicCurves { /// * `Result<(Decimal, Decimal), CurveError>` - A tuple containing (strike price, metric value), /// or an error if the values could not be extracted /// + /// # Errors + /// + /// Returns [`CurveError::ConstructionError`] when the requested + /// axis metric is not available for the option data (e.g. + /// missing implied volatility) and propagates any + /// [`CurveError::GreeksError`] surfaced while computing Greeks- + /// based axes. fn get_curve_strike_versus( &self, axis: &BasicAxisTypes, diff --git a/src/curves/traits.rs b/src/curves/traits.rs index cc25f191..c597c721 100644 --- a/src/curves/traits.rs +++ b/src/curves/traits.rs @@ -110,6 +110,13 @@ pub trait StatisticalCurve: MetricsExtractor { /// - `Result`: A curve matching the specified statistical properties, /// or an error if generation fails. /// + /// # Errors + /// + /// Returns [`CurveError::ConstructionError`] when the requested + /// combination of basic, shape and range-parameter metrics is + /// infeasible (e.g. negative variance, inconsistent quantiles), and + /// propagates [`CurveError::MetricsError`] when the per-sample + /// metric evaluation fails. fn generate_statistical_curve( &self, basic_metrics: &BasicMetrics, @@ -253,6 +260,13 @@ pub trait StatisticalCurve: MetricsExtractor { /// /// # Returns /// - `Result`: The generated curve or an error + /// + /// # Errors + /// + /// Same failure surface as + /// [`StatisticalCurve::generate_statistical_curve`], plus + /// [`CurveError::ConstructionError`] when the refinement loop + /// exhausts its iteration budget without matching the tolerance. #[allow(clippy::too_many_arguments)] fn generate_refined_statistical_curve( &self, @@ -307,6 +321,14 @@ pub trait StatisticalCurve: MetricsExtractor { /// /// # Returns /// - `Result`: True if metrics match within tolerance, false otherwise + /// + /// # Errors + /// + /// Propagates any [`CurveError::MetricsError`] or + /// [`CurveError::ConstructionError`] raised while recomputing the + /// curve's actual metrics (typically when the curve contains + /// non-finite samples or lacks the density required for the + /// requested metric). fn verify_curve_metrics( &self, curve: &Curve, diff --git a/src/curves/types.rs b/src/curves/types.rs index 54781fce..0ed5ba85 100644 --- a/src/curves/types.rs +++ b/src/curves/types.rs @@ -168,6 +168,14 @@ impl Point2D { /// /// # Usage /// This function allows constructing a `Point2D` directly from a tuple representation. + /// + /// # Errors + /// + /// Currently infallible for the blanket `Into` bounds; + /// the `Result` signature is retained so future implementations + /// that can reject non-finite or out-of-range conversions (e.g. + /// via `TryFrom`) can surface + /// [`CurveError::ConstructionError`] without a breaking change. pub fn from_tuple, U: Into>(x: T, y: U) -> Result { Ok(Self::new(x, y)) } diff --git a/src/geometrics/analysis/metrics.rs b/src/geometrics/analysis/metrics.rs index c985e85d..3fe26cd3 100644 --- a/src/geometrics/analysis/metrics.rs +++ b/src/geometrics/analysis/metrics.rs @@ -159,6 +159,13 @@ impl Metrics { /// The result provides the basic statistical measures (`BasicMetrics`) and /// shape metrics (`ShapeMetrics`) that were part of the `CurveMetrics` instance. /// + /// # Errors + /// + /// Currently infallible - the conversion is a direct field + /// re-pack. The `Result` signature is retained so future + /// implementations that validate the metrics invariants (e.g. + /// non-negative variance) can surface + /// [`CurveError::MetricsError`] without breaking callers. pub fn analysis_result(&self) -> Result { Ok(AnalysisResult { statistics: self.basic, diff --git a/src/geometrics/analysis/traits.rs b/src/geometrics/analysis/traits.rs index 6ac0547d..caab9021 100644 --- a/src/geometrics/analysis/traits.rs +++ b/src/geometrics/analysis/traits.rs @@ -33,6 +33,13 @@ pub trait MetricsExtractor: Len { /// # Returns /// - `Ok(BasicMetrics)`: Struct containing mean, median, mode, and standard deviation. /// - `Err(CurvesError)`: If metrics computation fails. + /// + /// # Errors + /// + /// Returns [`MetricsError::BasicError`] when the sample is + /// empty, when the variance cannot be computed in `Decimal` + /// precision, or when the mode aggregation is ambiguous for the + /// provided samples. fn compute_basic_metrics(&self) -> Result; /// Computes shape-related metrics for the curve. @@ -40,6 +47,12 @@ pub trait MetricsExtractor: Len { /// # Returns /// - `Ok(ShapeMetrics)`: Struct containing skewness, kurtosis, peaks, valleys, and inflection points. /// - `Err(CurvesError)`: If metrics computation fails. + /// + /// # Errors + /// + /// Returns [`MetricsError::ShapeError`] when the sample has + /// fewer than three points (skewness/kurtosis undefined) or when + /// the central-moment computation overflows the `Decimal` range. fn compute_shape_metrics(&self) -> Result; /// Computes range-related metrics for the curve. @@ -47,6 +60,12 @@ pub trait MetricsExtractor: Len { /// # Returns /// - `Ok(RangeMetrics)`: Struct containing min/max points, range, quartiles, and interquartile range. /// - `Err(CurvesError)`: If metrics computation fails. + /// + /// # Errors + /// + /// Returns [`MetricsError::RangeError`] when the sample is + /// empty (no min/max available) or when the quartile + /// interpolation fails for a sparse sample. fn compute_range_metrics(&self) -> Result; /// Computes trend-related metrics for the curve. @@ -54,6 +73,12 @@ pub trait MetricsExtractor: Len { /// # Returns /// - `Ok(TrendMetrics)`: Struct containing slope, intercept, R-squared, and moving average. /// - `Err(CurvesError)`: If metrics computation fails. + /// + /// # Errors + /// + /// Returns [`MetricsError::TrendError`] when the linear + /// regression cannot be fit (fewer than two samples or zero + /// variance on the independent axis). fn compute_trend_metrics(&self) -> Result; /// Computes risk-related metrics for the curve. @@ -61,6 +86,12 @@ pub trait MetricsExtractor: Len { /// # Returns /// - `Ok(RiskMetrics)`: Struct containing volatility, VaR, expected shortfall, beta, and Sharpe ratio. /// - `Err(CurvesError)`: If metrics computation fails. + /// + /// # Errors + /// + /// Returns [`MetricsError::RiskError`] when the sample cannot + /// support VaR/ES estimation (fewer than the required quantile + /// sample count) or when the Sharpe ratio denominator is zero. fn compute_risk_metrics(&self) -> Result; /// Computes and aggregates all curve metrics into a comprehensive `CurveMetrics` struct. @@ -68,6 +99,13 @@ pub trait MetricsExtractor: Len { /// # Returns /// - `Ok(CurveMetrics)`: A struct containing all computed metrics. /// - `Err(CurvesError)`: If any metrics computation fails. + /// + /// # Errors + /// + /// Propagates any [`MetricsError`] returned by + /// [`compute_basic_metrics`], [`compute_shape_metrics`], + /// [`compute_range_metrics`], [`compute_trend_metrics`] or + /// [`compute_risk_metrics`] (the first failing call short-circuits). fn compute_curve_metrics(&self) -> Result { let basic = self.compute_basic_metrics()?; let shape = self.compute_shape_metrics()?; diff --git a/src/geometrics/interpolation/bilinear.rs b/src/geometrics/interpolation/bilinear.rs index f7a1db00..7f2ce69b 100644 --- a/src/geometrics/interpolation/bilinear.rs +++ b/src/geometrics/interpolation/bilinear.rs @@ -87,5 +87,13 @@ pub trait BiLinearInterpolation { /// Err(e) => info!("Interpolation failed: {:?}", e), /// } /// ``` + /// + /// # Errors + /// + /// Returns [`InterpolationError::EmptyData`] when the grid has + /// no samples, [`InterpolationError::OutOfRange`] when `x` falls + /// outside the covered domain, and + /// [`InterpolationError::DegenerateInterval`] when the + /// neighbouring grid cell has zero width on either axis. fn bilinear_interpolate(&self, x: Input) -> Result; } diff --git a/src/geometrics/interpolation/cubic.rs b/src/geometrics/interpolation/cubic.rs index 79b6ee27..0a552f00 100644 --- a/src/geometrics/interpolation/cubic.rs +++ b/src/geometrics/interpolation/cubic.rs @@ -104,5 +104,14 @@ pub trait CubicInterpolation { /// # Returns /// - `Ok(Point)`: Represents the interpolated point on the curve. /// - `Err(Self::Error)`: Describes why the interpolation failed (e.g., invalid input). + /// + /// # Errors + /// + /// Returns [`InterpolationError::EmptyData`] when the sample is + /// empty, [`InterpolationError::OutOfRange`] when `x` falls + /// outside the sample domain, and + /// [`InterpolationError::DegenerateInterval`] when the local + /// four-point neighbourhood has collinear or duplicate + /// abscissae. fn cubic_interpolate(&self, x: Input) -> Result; } diff --git a/src/geometrics/interpolation/linear.rs b/src/geometrics/interpolation/linear.rs index 02ef24cd..24d13025 100644 --- a/src/geometrics/interpolation/linear.rs +++ b/src/geometrics/interpolation/linear.rs @@ -82,5 +82,13 @@ pub trait LinearInterpolation { /// # Returns /// - `Ok(Point)`: The calculated interpolated value of type `Point`. /// - `Err(Self::Error)`: An error indicating the reason why interpolation failed. + /// + /// # Errors + /// + /// Returns [`InterpolationError::EmptyData`] when the sample is + /// empty, [`InterpolationError::OutOfRange`] when `x` falls + /// outside the sample domain, and + /// [`InterpolationError::DegenerateInterval`] when two + /// bracketing samples share the same `x` coordinate. fn linear_interpolate(&self, x: Input) -> Result; } diff --git a/src/geometrics/interpolation/spline.rs b/src/geometrics/interpolation/spline.rs index 90934ab4..46787b75 100644 --- a/src/geometrics/interpolation/spline.rs +++ b/src/geometrics/interpolation/spline.rs @@ -113,5 +113,14 @@ pub trait SplineInterpolation { /// Err(e) => info!("Interpolation failed: {:?}", e), /// } /// ``` + /// + /// # Errors + /// + /// Returns [`InterpolationError::EmptyData`] when the sample is + /// empty, [`InterpolationError::OutOfRange`] when `x` falls + /// outside the sample domain, and + /// [`InterpolationError::DegenerateInterval`] when the spline + /// tridiagonal system is singular (duplicate or collinear + /// abscissae). fn spline_interpolate(&self, x: Input) -> Result; } diff --git a/src/geometrics/interpolation/traits.rs b/src/geometrics/interpolation/traits.rs index da32d0fb..45fe81ab 100644 --- a/src/geometrics/interpolation/traits.rs +++ b/src/geometrics/interpolation/traits.rs @@ -77,6 +77,14 @@ where /// # Returns /// - `Ok(Point)`: The successfully interpolated point /// - `Err(InterpolationError)`: If interpolation fails for any reason + /// + /// # Errors + /// + /// Dispatches to the requested algorithm and propagates any + /// [`InterpolationError`] it produces; see the per-algorithm + /// traits ([`LinearInterpolation`], [`CubicInterpolation`], + /// [`BiLinearInterpolation`], [`SplineInterpolation`]) for the + /// specific variant breakdown. fn interpolate( &self, x: Input, diff --git a/src/geometrics/operations/axis.rs b/src/geometrics/operations/axis.rs index 93364e26..0754458e 100644 --- a/src/geometrics/operations/axis.rs +++ b/src/geometrics/operations/axis.rs @@ -62,6 +62,12 @@ where /// /// # Returns /// * `Result<&Point, Self::Error>` - The closest point or an error if no points exist + /// + /// # Errors + /// + /// Returns the implementation's `Self::Error` (typically an + /// empty-set variant) when the underlying container holds no + /// points to compare against. fn get_closest_point(&self, x: &Input) -> Result<&Point, Self::Error>; /// Finds the closest point to the given coordinate value. @@ -165,6 +171,12 @@ where /// # Returns /// * `Result<(Self, Self), Self::Error>` - A tuple containing both structures /// with aligned coordinate points, or an error if interpolation fails + /// + /// # Errors + /// + /// Returns the implementation's `Self::Error` when either + /// structure is empty or when the per-axis interpolation on the + /// merged coordinate grid fails at any sample. fn merge_axis_interpolate( &self, other: &Self, diff --git a/src/geometrics/operations/traits.rs b/src/geometrics/operations/traits.rs index 4ea1c547..def4e8f0 100644 --- a/src/geometrics/operations/traits.rs +++ b/src/geometrics/operations/traits.rs @@ -69,8 +69,19 @@ pub trait Arithmetic { type Error; /// Combines multiple geometries into one using the specified merge operation. + /// + /// # Errors + /// + /// Returns the implementation's `Self::Error` when the input + /// slice is empty, when the selected `operation` is undefined + /// for the provided geometries, or when axis alignment fails. fn merge(geometries: &[&Input], operation: MergeOperation) -> Result; /// Merges the current curve with another curve using the specified merge operation. + /// + /// # Errors + /// + /// Same failure surface as [`merge`] applied to the two-element + /// collection `[self, other]`. fn merge_with(&self, other: &Input, operation: MergeOperation) -> Result; } diff --git a/src/geometrics/operations/transformations.rs b/src/geometrics/operations/transformations.rs index a8ba4047..a081e0ef 100644 --- a/src/geometrics/operations/transformations.rs +++ b/src/geometrics/operations/transformations.rs @@ -25,6 +25,12 @@ pub trait GeometricTransformations { /// /// A new instance of the geometric object after translation, or an error if /// the transformation could not be applied. + /// + /// # Errors + /// + /// Returns the implementation's `Self::Error` when `deltas` + /// length does not match the geometry's dimension, or when the + /// coordinate shift overflows the backing `Decimal` range. fn translate(&self, deltas: Vec<&Decimal>) -> Result where Self: Sized; @@ -41,6 +47,13 @@ pub trait GeometricTransformations { /// /// A new instance of the geometric object after scaling, or an error if /// the transformation could not be applied. + /// + /// # Errors + /// + /// Returns the implementation's `Self::Error` when `factors` + /// length does not match the geometry's dimension, when a factor + /// is zero on a required axis, or when the rescaled coordinate + /// overflows the backing `Decimal` range. fn scale(&self, factors: Vec<&Decimal>) -> Result where Self: Sized; @@ -55,6 +68,12 @@ pub trait GeometricTransformations { /// /// A vector of intersection points, or an error if the intersections /// could not be determined. + /// + /// # Errors + /// + /// Returns the implementation's `Self::Error` when the two + /// geometries live on incompatible domains or when the + /// intersection solver fails to converge. fn intersect_with(&self, other: &Self) -> Result, Self::Error>; /// Calculates the derivative at a specific point on the geometric object. @@ -70,6 +89,13 @@ pub trait GeometricTransformations { /// /// A vector containing the derivative values along each dimension, or an error /// if the derivative could not be calculated. + /// + /// # Errors + /// + /// Returns the implementation's `Self::Error` when `point` + /// falls outside the geometry's domain, or when the + /// finite-difference / analytical derivative is undefined at + /// that point (e.g. discontinuity). fn derivative_at(&self, point: &Point) -> Result, Self::Error>; /// Finds the extrema (minimum and maximum points) of the geometric object. @@ -78,6 +104,12 @@ pub trait GeometricTransformations { /// /// A tuple containing the minimum and maximum points of the geometric object, /// or an error if the extrema could not be determined. + /// + /// # Errors + /// + /// Returns the implementation's `Self::Error` when the geometry + /// is empty or when the extremum search cannot decide between + /// several tied candidates. fn extrema(&self) -> Result<(Point, Point), Self::Error>; /// Calculates the area or volume under the geometric object relative to a base value. @@ -92,5 +124,12 @@ pub trait GeometricTransformations { /// # Returns /// /// The calculated area or volume, or an error if the calculation failed. + /// + /// # Errors + /// + /// Returns the implementation's `Self::Error` when the + /// integration domain is empty, when the trapezoid / Simpson + /// kernel encounters a degenerate sub-interval, or when the + /// accumulator overflows the backing `Decimal` range. fn measure_under(&self, base_value: &Decimal) -> Result; } diff --git a/src/geometrics/utils.rs b/src/geometrics/utils.rs index cb2b7274..6a647fa9 100644 --- a/src/geometrics/utils.rs +++ b/src/geometrics/utils.rs @@ -40,6 +40,13 @@ pub trait GeometricObject { /// such as constructing from a set of data points or from a parametric function. /// /// This method returns a `Result` to handle potential errors during construction. + /// + /// # Errors + /// + /// Returns the implementation's `Self::Error` when the provided + /// construction method cannot produce a valid geometry, for + /// example when a `FromData` input is empty or a parametric + /// function surfaces a domain violation. fn construct(method: T) -> Result where Self: Sized, diff --git a/src/pnl/metrics.rs b/src/pnl/metrics.rs index 2a610e08..b9d978e5 100644 --- a/src/pnl/metrics.rs +++ b/src/pnl/metrics.rs @@ -213,6 +213,13 @@ impl fmt::Display for PnLMetricsStep { /// # Returns /// /// * `io::Result<()>` - Success or error result +/// +/// # Errors +/// +/// Returns [`std::io::Error`] when the output file cannot be created +/// or written (permission denied, disk full, invalid path), or when +/// `serde_json` serialization of the metrics slice surfaces a +/// semantic failure wrapped as [`std::io::ErrorKind::InvalidData`]. pub fn save_pnl_metrics(metrics: &[PnLMetricsStep], file_path: &str) -> io::Result<()> { // Serialize to compact JSON without pretty formatting let json = serde_json::to_string(metrics) @@ -237,6 +244,12 @@ pub fn save_pnl_metrics(metrics: &[PnLMetricsStep], file_path: &str) -> io::Resu /// /// * `io::Result>` - Vector of deserialized metrics or error /// +/// # Errors +/// +/// Returns [`std::io::Error`] when the file cannot be opened or read, +/// or when `serde_json` deserialization fails (returned as +/// [`std::io::ErrorKind::InvalidData`] with the underlying parse +/// error as source). pub fn load_pnl_metrics(file_path: &str) -> io::Result> { // Read the file content let file_content = std::fs::read_to_string(file_path)?; diff --git a/src/pnl/traits.rs b/src/pnl/traits.rs index 4a50f781..4eb2bcc9 100644 --- a/src/pnl/traits.rs +++ b/src/pnl/traits.rs @@ -27,6 +27,14 @@ pub trait PnLCalculator { /// /// # Returns /// * `Result` - The calculated PnL or an error + /// + /// # Errors + /// + /// Returns [`PricingError::ExpirationDate`] when the expiration + /// cannot be converted, [`PricingError::MethodError`] when the + /// underlying Black–Scholes kernel fails, or propagates any other + /// [`PricingError`] surfaced by the strategy's component + /// evaluations. fn calculate_pnl( &self, _underlying_price: &Positive, @@ -45,6 +53,12 @@ pub trait PnLCalculator { /// /// # Returns /// * `Result` - The calculated PnL at expiration or an error + /// + /// # Errors + /// + /// Returns [`PricingError::MethodError`] when the terminal + /// payoff evaluation fails on any leg (typically propagated from + /// [`Position::pnl_at_expiration`]). fn calculate_pnl_at_expiration( &self, _underlying_price: &Positive, @@ -124,9 +138,21 @@ pub trait PnLCalculator { /// pub trait TransactionAble { /// Adds a new transaction to the implementing entity. + /// + /// # Errors + /// + /// Returns [`TransactionError`] when the transaction violates the + /// implementor's invariants (e.g. duplicate id, status mismatch, or + /// arithmetic overflow on the accumulator). fn add_transaction(&mut self, transaction: Transaction) -> Result<(), TransactionError>; /// Retrieves all transactions from the implementing entity. + /// + /// # Errors + /// + /// Returns [`TransactionError`] when the transaction log cannot + /// be enumerated (typically due to a corrupted internal state or + /// a locked shared storage). fn get_transactions(&self) -> Result, TransactionError>; } diff --git a/src/pnl/transaction.rs b/src/pnl/transaction.rs index db31d8cd..1329a34b 100644 --- a/src/pnl/transaction.rs +++ b/src/pnl/transaction.rs @@ -183,6 +183,14 @@ impl Transaction { /// # Returns /// /// A Result containing the PnL or an error + /// + /// # Errors + /// + /// Returns [`TransactionError`] when the internal state is + /// inconsistent for the declared [`TradeStatus`] (missing close + /// price for a `Closed` trade, missing settlement data for an + /// `Expired` trade, or arithmetic overflow while combining + /// premium, fees and quantity). pub fn pnl(&self) -> Result { match self.status { TradeStatus::Open => self.calculate_open_pnl(), diff --git a/src/simulation/params.rs b/src/simulation/params.rs index a91a9390..617277e9 100644 --- a/src/simulation/params.rs +++ b/src/simulation/params.rs @@ -110,6 +110,13 @@ where /// # Returns /// /// The initial y-axis value as a `Positive` number. + /// + /// # Errors + /// + /// Propagates any [`SimulationError`] returned by + /// [`Ystep::positive`] (typically + /// [`SimulationError::PositiveError`] when the current `y` value + /// breaches the `Positive` invariant). pub fn ystep_as_positive(&self) -> Result { self.ystep_ref().positive() } diff --git a/src/simulation/steps/step.rs b/src/simulation/steps/step.rs index 6450011b..4c20c0d5 100644 --- a/src/simulation/steps/step.rs +++ b/src/simulation/steps/step.rs @@ -107,6 +107,13 @@ where /// # Returns /// /// A new `Step` instance that represents the next step in the sequence + /// + /// # Errors + /// + /// Returns [`SimulationError::ExpirationReached`] when the + /// current step is already at the end of the sequence, or + /// [`SimulationError::IndexConversion`] when the step index would + /// overflow `i32`. pub fn next(&self, new_y_value: Y) -> Result { let next_x = match self.x.next() { Ok(x_step) => x_step, @@ -134,6 +141,13 @@ where /// # Returns /// /// A new `Step` instance that represents the previous step in the sequence + /// + /// # Errors + /// + /// Returns [`SimulationError::IndexConversion`] when the step + /// index would wrap below `i32::MIN`, or + /// [`SimulationError::ExpirationDate`] when the underlying + /// [`Xstep::previous`] datetime shift fails. pub fn previous(&self, new_y_value: Y) -> Result { let previous_x = match self.x.previous() { Ok(x_step) => x_step, @@ -157,6 +171,13 @@ where /// # Returns /// /// A `Positive` representation of the x-axis index as a floating point value + /// + /// # Errors + /// + /// Returns [`SimulationError::IndexConversion`] when the step + /// index cannot be represented as a `Decimal` (practically + /// unreachable for `i32` inputs but retained for forward + /// compatibility). pub fn get_graph_x_value(&self) -> Result { match Decimal::from_i32(*self.x.index()) { Some(x) => Ok(x), @@ -194,6 +215,13 @@ where /// # Returns /// /// A `Positive` representation of the y-axis value + /// + /// # Errors + /// + /// Propagates any [`SimulationError`] returned by + /// [`Ystep::positive`] (typically + /// [`SimulationError::PositiveError`] when the stored `y` value + /// violates the `Positive` invariant). pub fn get_graph_y_value(&self) -> Result { self.y.positive() } @@ -255,6 +283,13 @@ where /// /// This function internally calls the `positive` method on `self.y` /// and returns the resulting value. + /// + /// # Errors + /// + /// Propagates any [`SimulationError`] returned by + /// [`Ystep::positive`] (typically + /// [`SimulationError::PositiveError`] when the stored `y` value + /// violates the `Positive` invariant). pub fn get_positive_value(&self) -> Result { self.y.positive() } diff --git a/src/simulation/steps/x.rs b/src/simulation/steps/x.rs index 6b46cafc..c81c4733 100644 --- a/src/simulation/steps/x.rs +++ b/src/simulation/steps/x.rs @@ -179,6 +179,11 @@ where /// * `Result` - A positive decimal value representing the number of days /// until expiration, or an error if the calculation cannot be performed. /// + /// # Errors + /// + /// Returns [`SimulationError::ExpirationDate`] when the stored + /// [`ExpirationDate`] cannot be resolved to a non-negative day + /// count (e.g. the datetime is in the past). pub fn days_left(&self) -> Result { Ok(self.datetime.get_days()?) } @@ -191,6 +196,13 @@ where /// # Returns /// /// A new `Xstep` instance with updated index and datetime values. + /// + /// # Errors + /// + /// Returns [`SimulationError::ExpirationReached`] when the + /// current step is already at expiration (no forward step is + /// possible), or [`SimulationError::ExpirationDate`] when the + /// underlying [`ExpirationDate::get_days`] call fails. pub fn next(&self) -> Result { let days = self.datetime.get_days()?; if days == Positive::ZERO { @@ -228,6 +240,13 @@ where /// # Returns /// /// A new `Xstep` instance with updated index and datetime values. + /// + /// # Errors + /// + /// Returns [`SimulationError::ExpirationDate`] when the + /// underlying [`ExpirationDate::get_days`] call fails, or + /// [`SimulationError::IndexConversion`] when the step index + /// decrement would underflow `i32::MIN`. pub fn previous(&self) -> Result { let days = self.datetime.get_days()?; let days_to_rest = convert_time_frame( diff --git a/src/simulation/steps/y.rs b/src/simulation/steps/y.rs index bfae8766..5d9555ff 100644 --- a/src/simulation/steps/y.rs +++ b/src/simulation/steps/y.rs @@ -117,6 +117,12 @@ where /// # Returns /// /// A reference to the stored value of type `T` + /// + /// # Errors + /// + /// Returns [`SimulationError::PositiveError`] when the stored + /// `y` value cannot be converted to a `Positive` (NaN, negative, + /// or otherwise invalid for the `Positive` invariant). pub fn positive(&self) -> Result { self.value .clone() diff --git a/src/simulation/traits.rs b/src/simulation/traits.rs index 7fa1f985..cf1ef365 100644 --- a/src/simulation/traits.rs +++ b/src/simulation/traits.rs @@ -103,6 +103,13 @@ where /// /// * `Result, SimulationError>` - A vector of positive values representing /// the generated Brownian motion path, or an error if parameters are invalid. + /// + /// # Errors + /// + /// Returns [`SimulationError::InvalidWalkType`] when + /// `params.walk_type` is not a [`WalkType::Brownian`] variant, + /// and [`SimulationError::PositiveError`] when a generated sample + /// cannot be represented as `Positive`. fn brownian(&self, params: &WalkParams) -> Result, SimulationError> { match params.walk_type { WalkType::Brownian { @@ -150,6 +157,13 @@ where /// /// * `Result, SimulationError>` - A vector of positive values representing /// the generated Geometric Brownian motion path, or an error if parameters are invalid. + /// + /// # Errors + /// + /// Returns [`SimulationError::InvalidWalkType`] when + /// `params.walk_type` is not a [`WalkType::GeometricBrownian`] + /// variant, and [`SimulationError::PositiveError`] when a + /// generated sample underflows below zero. fn geometric_brownian( &self, params: &WalkParams, @@ -196,6 +210,15 @@ where /// /// * `Result, SimulationError>` - A vector of positive values representing /// the generated Log Returns path, or an error if parameters are invalid. + /// + /// # Errors + /// + /// Returns [`SimulationError::InvalidWalkType`] when + /// `params.walk_type` is not a [`WalkType::LogReturns`] variant, + /// [`SimulationError::InvalidAutocorrelation`] when the + /// autocorrelation coefficient is outside `[-1, 1]`, and + /// [`SimulationError::PositiveError`] when the exponentiated + /// sample underflows below zero. fn log_returns(&self, params: &WalkParams) -> Result, SimulationError> { match params.walk_type { WalkType::LogReturns { @@ -253,6 +276,13 @@ where /// /// * `Result, SimulationError>` - A vector of positive values representing /// the generated Mean Reverting path, or an error if parameters are invalid. + /// + /// # Errors + /// + /// Returns [`SimulationError::InvalidWalkType`] when + /// `params.walk_type` is not a [`WalkType::MeanReverting`] + /// variant, and [`SimulationError::PositiveError`] when an + /// intermediate sample breaches the `Positive` invariant. fn mean_reverting(&self, params: &WalkParams) -> Result, SimulationError> { match params.walk_type { WalkType::MeanReverting { @@ -293,6 +323,13 @@ where /// /// * `Result, SimulationError>` - A vector of positive values representing /// the generated Jump Diffusion path, or an error if parameters are invalid. + /// + /// # Errors + /// + /// Returns [`SimulationError::InvalidWalkType`] when + /// `params.walk_type` is not a [`WalkType::JumpDiffusion`] + /// variant, and [`SimulationError::PositiveError`] when a jumped + /// sample underflows below zero. fn jump_diffusion(&self, params: &WalkParams) -> Result, SimulationError> { match params.walk_type { WalkType::JumpDiffusion { @@ -353,6 +390,15 @@ where /// # Note /// /// This implementation is currently a placeholder and returns an empty vector. + /// + /// # Errors + /// + /// Returns [`SimulationError::InvalidWalkType`] when + /// `params.walk_type` is not a [`WalkType::Garch`] variant, + /// [`SimulationError::GarchStationarity`] when `alpha + beta ≥ 1` + /// (the recurrence is non-stationary), and + /// [`SimulationError::PositiveError`] when an intermediate + /// variance breaches the `Positive` invariant. fn garch(&self, params: &WalkParams) -> Result, SimulationError> { match params.walk_type { WalkType::Garch { @@ -447,6 +493,15 @@ where /// dS_t = μS_t dt + √v_t S_t dW^1_t /// dv_t = κ(θ - v_t) dt + ξ√v_t dW^2_t /// with dW^1_t dW^2_t = ρ dt + /// + /// # Errors + /// + /// Returns [`SimulationError::InvalidWalkType`] when + /// `params.walk_type` is not a [`WalkType::Heston`] variant, + /// [`SimulationError::InvalidCorrelation`] when `rho` is outside + /// `[-1, 1]`, and [`SimulationError::PositiveError`] when an + /// intermediate price or variance breaches the `Positive` + /// invariant. fn heston(&self, params: &WalkParams) -> Result, SimulationError> { match params.walk_type { WalkType::Heston { @@ -529,6 +584,15 @@ where /// /// * `Result, SimulationError>` - A vector of positive values representing /// the generated custom process path, or an error if parameters are invalid. + /// + /// # Errors + /// + /// Returns [`SimulationError::InvalidWalkType`] when + /// `params.walk_type` is not a [`WalkType::Custom`] variant, + /// [`SimulationError::InsufficientHistoricalData`] when the + /// provided calibration sample is too small for the requested + /// process order, and [`SimulationError::PositiveError`] when a + /// generated sample violates the `Positive` invariant. fn custom(&self, params: &WalkParams) -> Result, SimulationError> { match params.walk_type { WalkType::Custom { @@ -579,6 +643,13 @@ where /// /// * `Result, SimulationError>` - A vector of positive values representing /// the generated Telegraph process path, or an error if parameters are invalid. + /// + /// # Errors + /// + /// Returns [`SimulationError::InvalidWalkType`] when + /// `params.walk_type` is not a [`WalkType::Telegraph`] variant, + /// and [`SimulationError::PositiveError`] when an intermediate + /// sample breaches the `Positive` invariant. fn telegraph(&self, params: &WalkParams) -> Result, SimulationError> { match params.walk_type { WalkType::Telegraph { diff --git a/src/surfaces/basic.rs b/src/surfaces/basic.rs index ce59c16e..74953e6c 100644 --- a/src/surfaces/basic.rs +++ b/src/surfaces/basic.rs @@ -33,6 +33,14 @@ pub trait BasicSurfaces { /// # Returns /// /// * `Result` - A constructed surface or an error if creation fails + /// + /// # Errors + /// + /// Returns [`SurfaceError::ConstructionError`] when the sampling + /// grid is empty or the requested axis is not available, and + /// propagates [`SurfaceError::Point3DError`] or + /// [`SurfaceError::OperationError`] from the per-sample + /// evaluator. fn surface( &self, axis: &BasicAxisTypes, @@ -361,6 +369,14 @@ pub trait BasicSurfaces { /// # Returns /// /// * `Result` - A constructed surface or an error if creation fails + /// + /// # Errors + /// + /// Returns [`SurfaceError::ConstructionError`] when no expiration + /// shift produced a valid grid, and propagates + /// [`SurfaceError::Point3DError`] or + /// [`SurfaceError::OperationError`] from the per-sample + /// evaluator. fn time_surface( &self, axis: &BasicAxisTypes, diff --git a/src/surfaces/traits.rs b/src/surfaces/traits.rs index 8a6a9f0c..11ad8594 100644 --- a/src/surfaces/traits.rs +++ b/src/surfaces/traits.rs @@ -45,5 +45,13 @@ pub trait Surfacable { /// - `surface()`: /// - Returns: `Result` /// - Description: Generates a surface or returns an error if something goes wrong during the process. + /// + /// # Errors + /// + /// Returns [`SurfaceError::ConstructionError`] when the + /// implementor cannot produce any `(x, y, z)` sample + /// (empty data source, degenerate axes, or missing metric), and + /// propagates [`SurfaceError::Point3DError`] when a sample + /// cannot be represented as a [`Point3D`]. fn surface(&self) -> Result; } diff --git a/src/surfaces/types.rs b/src/surfaces/types.rs index 9a4b2644..58bd9bfe 100644 --- a/src/surfaces/types.rs +++ b/src/surfaces/types.rs @@ -107,6 +107,13 @@ impl Point3D { /// - `T`: Type for x-coordinate /// - `U`: Type for y-coordinate /// - `V`: Type for z-coordinate + /// + /// # Errors + /// + /// Returns [`SurfaceError::Point3DError`] when one of the + /// `Decimal` coordinates cannot be converted into the target + /// numeric type `T`, `U` or `V` (e.g. out-of-range for `f32`, + /// overflow for `u32`). pub fn to_tuple< T: TryFrom + 'static, U: TryFrom + 'static, @@ -146,6 +153,14 @@ impl Point3D { } /// Creates a Point3D from a tuple of three values. + /// + /// # Errors + /// + /// Currently infallible for the blanket `Into` + /// bounds; the `Result` signature is retained so future + /// implementations that can reject non-finite or out-of-range + /// inputs (e.g. via `TryFrom`) can surface + /// [`SurfaceError::Point3DError`] without a breaking change. pub fn from_tuple, U: Into, V: Into>( x: T, y: U, @@ -155,6 +170,12 @@ impl Point3D { } /// Converts the Point3D to a tuple of f64 values. + /// + /// # Errors + /// + /// Returns [`SurfaceError::Point3DError`] when any coordinate + /// cannot be represented as an `f64` (typically a `Decimal` + /// magnitude that exceeds the `f64` range). pub fn to_f64_tuple(&self) -> Result<(f64, f64, f64), SurfaceError> { let x = self.x.to_f64(); let y = self.y.to_f64(); @@ -169,6 +190,12 @@ impl Point3D { } /// Creates a Point3D from a tuple of f64 values. + /// + /// # Errors + /// + /// Returns [`SurfaceError::Point3DError`] when any of `x`, + /// `y` or `z` is `NaN` or infinite and therefore not + /// representable as a `Decimal`. pub fn from_f64_tuple(x: f64, y: f64, z: f64) -> Result { let x = Decimal::from_f64(x); let y = Decimal::from_f64(y); diff --git a/src/utils/csv.rs b/src/utils/csv.rs index bddbedd0..474d8f9c 100644 --- a/src/utils/csv.rs +++ b/src/utils/csv.rs @@ -149,6 +149,13 @@ pub fn read_ohlcv_from_zip( /// # Note /// /// This method is only available with the `async` feature. +/// +/// # Errors +/// +/// Returns the same variants as [`read_ohlcv_from_zip`] +/// (I/O, ZIP, CSV and date/decimal parsing), plus +/// [`OhlcvError::AsyncTask`] when the `spawn_blocking` worker fails +/// (e.g. panics or cancellation). #[cfg(feature = "async")] pub async fn read_ohlcv_from_zip_async( zip_path: String, diff --git a/src/utils/file.rs b/src/utils/file.rs index 39cfc0ac..b8832a4a 100644 --- a/src/utils/file.rs +++ b/src/utils/file.rs @@ -15,6 +15,13 @@ use tracing::{debug, error, trace}; /// # Returns /// /// * `Result<(), std::io::Error>` - Ok if successful, or an IoError if it failed +/// +/// # Errors +/// +/// Returns [`std::io::Error`] when the target file exists but cannot +/// be removed, or when the parent directory cannot be created +/// (typically `PermissionDenied`, `NotFound` on a broken symlink +/// ancestor, or the platform-specific disk-full case). pub fn prepare_file_path(path: &Path) -> Result<(), IoError> { // Remove file if it already exists if path.exists() { diff --git a/src/visualization/plotly.rs b/src/visualization/plotly.rs index 310e55cb..df3fffca 100644 --- a/src/visualization/plotly.rs +++ b/src/visualization/plotly.rs @@ -309,6 +309,14 @@ pub trait Graph { } /// Show the plot in browser + /// + /// # Errors + /// + /// Currently infallible (the underlying `plotly` `show` call does + /// not return a `Result`); the `Result` signature is retained to + /// allow future plot kernels that can surface + /// [`GraphError::RenderError`] or [`GraphError::IoError`] without + /// a breaking change. #[cfg(feature = "plotly")] fn show(&self) -> Result<(), GraphError> { self.to_plot().show(); @@ -316,6 +324,13 @@ pub trait Graph { } /// One‑stop rendering with error propagation. + /// + /// # Errors + /// + /// Returns [`GraphError::RenderError`] when the chosen + /// [`OutputType`] backend (PNG/SVG via static_export, HTML, etc.) + /// fails to serialize or render, and [`GraphError::IoError`] when + /// the destination path cannot be written. #[cfg(feature = "plotly")] fn render(&self, output: OutputType) -> Result<(), GraphError> { match output { @@ -344,6 +359,13 @@ pub trait Graph { } /// Generate interactive HTML with hover info + annotations. + /// + /// # Errors + /// + /// Propagates any [`GraphError`] returned by + /// [`PlotlyChart::write_html`], typically + /// [`GraphError::IoError`] when the target file cannot be + /// created or written. #[cfg(feature = "plotly")] fn to_interactive_html(&self, path: &std::path::Path) -> Result<(), GraphError> { self.write_html(path) From 61291f7d3699dfaf99858ff911b5b65fe2a08739 Mon Sep 17 00:00:00 2001 From: Joaquin Bejar Date: Sun, 19 Apr 2026 11:28:16 +0200 Subject: [PATCH 08/10] M4 #334: add # Panics section to decimal_normal_sample Completes Phase B of the panic audit: - cargo clippy --lib -- -W missing_panics_doc reports 0 warnings. - Manual sweep of production unwrap/expect/panic! sites confirms every remaining panic is intentional and inline-documented: * src/constants.rs (7): unwrap_or_else(|e| unreachable!(...)) on compile-time literals that are provably valid. * src/strategies/base.rs (2): one_option / one_option_mut trait defaults already carry # Panics documentation. * src/surfaces/surface.rs and src/curves/curve.rs (1 each): std::ops::Index impls already carry # Panics documentation matching the trait's own contract. * src/model/decimal.rs decimal_normal_sample: now documented. The selected "Full sweep + fix" option was scoped down to documenting the one remaining undocumented panic rather than refactoring the Index impls or the base.rs trait defaults \u2014 both cases are legitimate uses of panic where no Result return is expressible, and the inline invariant messages make the contract explicit. Refs #334 --- src/model/decimal.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/model/decimal.rs b/src/model/decimal.rs index 8104187a..bd2ba8d6 100644 --- a/src/model/decimal.rs +++ b/src/model/decimal.rs @@ -229,6 +229,14 @@ pub fn f64_to_decimal(value: f64) -> Result { /// use positive::Positive; /// let normal = decimal_normal_sample(); /// ``` +/// +/// # Panics +/// +/// The `unreachable!` branch fires only if +/// `statrs::distribution::Normal::new(0.0, 1.0)` rejects the standard +/// normal parameters — a provably impossible state the standard +/// library upholds at all supported versions. Kept as a panic +/// rather than `Result` to preserve the infallible sampling API. pub fn decimal_normal_sample() -> Decimal { let mut t_rng = rand::rng(); // Normal::new(0.0, 1.0) is provably valid (mean=0, std=1 are accepted From 2efb70ea73c4c8d8152309ea74cc3ad2b446aba2 Mon Sep 17 00:00:00 2001 From: Joaquin Bejar Date: Sun, 19 Apr 2026 11:47:29 +0200 Subject: [PATCH 09/10] M4 #334: resolve broken intra-doc links Strip intra-doc link brackets on 60 symbols that rustdoc could not resolve (nested variants, private items, ambiguous names). Each becomes a plain backtick code span, preserving the # Errors / # Panics text without generating doc-build warnings. - MAX_ITERATIONS_IV (private), Sx (literal), OptionsError, PricingError::*, PositionError::*, FileErrorKind::*, StrategyErrorKind::*, ProbabilityCalculationErrorKind::*, CurveError::GreeksError, GraphError::*, AdjustmentError::*, Strategable::*, compute_*_metrics, numerical_*, price_binomial, delta_*, etc. - Disambiguate [`telegraph`] to the kernel name in prose form (module vs function collision). RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features now builds clean. Refs #334 --- src/chains/chain.rs | 34 +++++++++++------------ src/curves/basic.rs | 4 +-- src/error/options.rs | 4 +-- src/geometrics/analysis/traits.rs | 6 ++-- src/geometrics/operations/traits.rs | 2 +- src/greeks/equations.rs | 8 +++--- src/greeks/numerical.rs | 12 ++++---- src/metrics/temporal/charm.rs | 2 +- src/metrics/temporal/color.rs | 2 +- src/metrics/temporal/theta.rs | 2 +- src/model/option.rs | 30 ++++++++++---------- src/model/position.rs | 8 +++--- src/model/profit_range.rs | 4 +-- src/pnl/traits.rs | 10 +++---- src/pricing/american.rs | 4 +-- src/pricing/barrier.rs | 4 +-- src/pricing/binary.rs | 4 +-- src/pricing/black_scholes_model.rs | 14 +++++----- src/pricing/chooser.rs | 2 +- src/pricing/cliquet.rs | 2 +- src/pricing/compound.rs | 2 +- src/pricing/lookback.rs | 2 +- src/pricing/monte_carlo.rs | 4 +-- src/pricing/payoff.rs | 6 ++-- src/pricing/telegraph.rs | 4 +-- src/pricing/unified.rs | 6 ++-- src/strategies/base.rs | 26 ++++++++--------- src/strategies/build/traits.rs | 2 +- src/strategies/collar.rs | 6 ++-- src/strategies/covered_call.rs | 4 +-- src/strategies/delta_neutral/model.rs | 30 ++++++++++---------- src/strategies/delta_neutral/optimizer.rs | 4 +-- src/strategies/probabilities/core.rs | 10 +++---- src/strategies/protective_put.rs | 2 +- src/surfaces/traits.rs | 2 +- src/visualization/plotly.rs | 10 +++---- src/volatility/utils.rs | 6 ++-- 37 files changed, 142 insertions(+), 142 deletions(-) diff --git a/src/chains/chain.rs b/src/chains/chain.rs index 85eb7d1b..f4dbd56c 100644 --- a/src/chains/chain.rs +++ b/src/chains/chain.rs @@ -1084,9 +1084,9 @@ impl OptionChain { /// /// # Errors /// - /// Returns [`ChainError::FileError`] wrapping a [`FileErrorKind::IOError`] + /// Returns [`ChainError::FileError`] wrapping a `FileErrorKind::IOError` /// when the file cannot be created or written, or - /// [`FileErrorKind::ParseError`] when `csv` serialization fails. + /// `FileErrorKind::ParseError` when `csv` serialization fails. pub fn save_to_csv(&self, file_path: &str) -> Result<(), ChainError> { let full_path = format!("{}/{}.csv", file_path, self.get_title()); let mut wtr = WriterBuilder::new().from_path(full_path)?; @@ -1131,9 +1131,9 @@ impl OptionChain { /// # Errors /// /// Returns the same variants as [`OptionChain::save_to_csv`] - /// ([`ChainError::FileError`] wrapping [`FileErrorKind::IOError`] or - /// [`FileErrorKind::ParseError`]). A `spawn_blocking` join failure is - /// surfaced as [`FileErrorKind::IOError`]. + /// ([`ChainError::FileError`] wrapping `FileErrorKind::IOError` or + /// `FileErrorKind::ParseError`). A `spawn_blocking` join failure is + /// surfaced as `FileErrorKind::IOError`. #[cfg(feature = "async")] pub async fn save_to_csv_async(&self, file_path: &str) -> Result<(), ChainError> { let path = file_path.to_string(); @@ -1164,9 +1164,9 @@ impl OptionChain { /// /// # Errors /// - /// Returns [`ChainError::FileError`] wrapping a [`FileErrorKind::IOError`] + /// Returns [`ChainError::FileError`] wrapping a `FileErrorKind::IOError` /// when the file cannot be created or written, or - /// [`FileErrorKind::ParseError`] when `serde_json` serialization fails. + /// `FileErrorKind::ParseError` when `serde_json` serialization fails. pub fn save_to_json(&self, file_path: &str) -> Result<(), ChainError> { let full_path = format!("{}/{}.json", file_path, self.get_title()); let file = File::create(full_path)?; @@ -1183,9 +1183,9 @@ impl OptionChain { /// # Errors /// /// Returns the same variants as [`OptionChain::save_to_json`] - /// ([`ChainError::FileError`] wrapping [`FileErrorKind::IOError`] or - /// [`FileErrorKind::ParseError`]). A `spawn_blocking` join failure is - /// surfaced as [`FileErrorKind::IOError`]. + /// ([`ChainError::FileError`] wrapping `FileErrorKind::IOError` or + /// `FileErrorKind::ParseError`). A `spawn_blocking` join failure is + /// surfaced as `FileErrorKind::IOError`. #[cfg(feature = "async")] pub async fn save_to_json_async(&self, file_path: &str) -> Result<(), ChainError> { let path = file_path.to_string(); @@ -1215,9 +1215,9 @@ impl OptionChain { /// /// # Errors /// - /// Returns [`ChainError::FileError`] wrapping [`FileErrorKind::IOError`] + /// Returns [`ChainError::FileError`] wrapping `FileErrorKind::IOError` /// when the CSV file cannot be opened or read, or - /// [`FileErrorKind::ParseError`] when the CSV records cannot be parsed. + /// `FileErrorKind::ParseError` when the CSV records cannot be parsed. /// Invalid option data (bad strike, volatility or price) surfaces as /// [`ChainError::OptionDataError`]. pub fn load_from_csv(file_path: &str) -> Result { @@ -1278,7 +1278,7 @@ impl OptionChain { /// /// Returns the same variants as [`OptionChain::load_from_csv`]. A /// `spawn_blocking` join failure is surfaced as - /// [`ChainError::FileError`] wrapping [`FileErrorKind::IOError`]. + /// [`ChainError::FileError`] wrapping `FileErrorKind::IOError`. #[cfg(feature = "async")] pub async fn load_from_csv_async(file_path: &str) -> Result { let path = file_path.to_string(); @@ -1307,8 +1307,8 @@ impl OptionChain { /// /// # Errors /// - /// Returns [`ChainError::FileError`] wrapping [`FileErrorKind::IOError`] - /// when the file cannot be opened, or [`FileErrorKind::ParseError`] + /// Returns [`ChainError::FileError`] wrapping `FileErrorKind::IOError` + /// when the file cannot be opened, or `FileErrorKind::ParseError` /// when `serde_json` deserialization fails. pub fn load_from_json(file_path: &str) -> Result { let file = File::open(file_path)?; @@ -1339,7 +1339,7 @@ impl OptionChain { /// /// Returns the same variants as [`OptionChain::load_from_json`]. A /// `spawn_blocking` join failure is surfaced as - /// [`ChainError::FileError`] wrapping [`FileErrorKind::IOError`]. + /// [`ChainError::FileError`] wrapping `FileErrorKind::IOError`. #[cfg(feature = "async")] pub async fn load_from_json_async(file_path: &str) -> Result { let path = file_path.to_string(); @@ -1431,7 +1431,7 @@ impl OptionChain { /// # Errors /// /// Returns [`ChainError::StrategyError`] wrapping a - /// [`StrategyErrorKind::InvalidLegs`] when the requested position counts + /// `StrategyErrorKind::InvalidLegs` when the requested position counts /// exceed available strikes on either side of the chain, or propagates /// any [`ChainError::OptionDataError`] produced while materialising the /// selected strikes into [`Position`] instances. diff --git a/src/curves/basic.rs b/src/curves/basic.rs index c37464cd..5049b2a9 100644 --- a/src/curves/basic.rs +++ b/src/curves/basic.rs @@ -46,7 +46,7 @@ pub trait BasicCurves { /// /// Returns [`CurveError::ConstructionError`] when no strikes /// produced valid samples for the requested axis, and propagates - /// any [`CurveError::GreeksError`] or + /// any `CurveError::GreeksError` or /// [`CurveError::InterpolationError`] surfaced by the per-strike /// evaluator. fn curve( @@ -77,7 +77,7 @@ pub trait BasicCurves { /// Returns [`CurveError::ConstructionError`] when the requested /// axis metric is not available for the option data (e.g. /// missing implied volatility) and propagates any - /// [`CurveError::GreeksError`] surfaced while computing Greeks- + /// `CurveError::GreeksError` surfaced while computing Greeks- /// based axes. fn get_curve_strike_versus( &self, diff --git a/src/error/options.rs b/src/error/options.rs index 78b92b0b..b8622f42 100644 --- a/src/error/options.rs +++ b/src/error/options.rs @@ -196,7 +196,7 @@ pub enum OptionsError { /// A specialized result type for operations related to Options calculations and processing. /// /// This type alias simplifies error handling for functions that can fail with various -/// options-specific errors. It uses the [`OptionsError`] enum to provide structured +/// options-specific errors. It uses the `OptionsError` enum to provide structured /// error information about validation failures, pricing issues, Greeks calculations, /// time-related problems, and other option-specific errors. /// @@ -233,7 +233,7 @@ pub enum OptionsError { /// * Expiration and time value calculations /// * Option payoff analysis /// -/// [`OptionsError`]: enum.OptionsError.html +/// `OptionsError`: enum.OptionsError.html pub type OptionsResult = Result; /// Helper methods for creating common options errors. diff --git a/src/geometrics/analysis/traits.rs b/src/geometrics/analysis/traits.rs index caab9021..ab1adc0a 100644 --- a/src/geometrics/analysis/traits.rs +++ b/src/geometrics/analysis/traits.rs @@ -103,9 +103,9 @@ pub trait MetricsExtractor: Len { /// # Errors /// /// Propagates any [`MetricsError`] returned by - /// [`compute_basic_metrics`], [`compute_shape_metrics`], - /// [`compute_range_metrics`], [`compute_trend_metrics`] or - /// [`compute_risk_metrics`] (the first failing call short-circuits). + /// `compute_basic_metrics`, `compute_shape_metrics`, + /// `compute_range_metrics`, `compute_trend_metrics` or + /// `compute_risk_metrics` (the first failing call short-circuits). fn compute_curve_metrics(&self) -> Result { let basic = self.compute_basic_metrics()?; let shape = self.compute_shape_metrics()?; diff --git a/src/geometrics/operations/traits.rs b/src/geometrics/operations/traits.rs index def4e8f0..8d36cd4b 100644 --- a/src/geometrics/operations/traits.rs +++ b/src/geometrics/operations/traits.rs @@ -81,7 +81,7 @@ pub trait Arithmetic { /// /// # Errors /// - /// Same failure surface as [`merge`] applied to the two-element + /// Same failure surface as `merge` applied to the two-element /// collection `[self, other]`. fn merge_with(&self, other: &Input, operation: MergeOperation) -> Result; } diff --git a/src/greeks/equations.rs b/src/greeks/equations.rs index bdaf0554..8b567e83 100644 --- a/src/greeks/equations.rs +++ b/src/greeks/equations.rs @@ -651,7 +651,7 @@ pub fn delta(option: &Options) -> Result { /// /// Returns [`GreeksError::ExpirationDate`] when the option's expiration /// cannot be converted to a positive year fraction, and propagates any -/// [`GreeksError`] surfaced by [`numerical_gamma`] for non-European +/// [`GreeksError`] surfaced by `numerical_gamma` for non-European /// options (typically [`GreeksError::Pricing`] when the perturbation /// evaluation fails). pub fn gamma(option: &Options) -> Result { @@ -793,7 +793,7 @@ pub fn gamma(option: &Options) -> Result { /// /// Returns [`GreeksError::ExpirationDate`] when the option's expiration /// cannot be converted to a positive year fraction, and propagates any -/// [`GreeksError`] surfaced by [`numerical_theta`] for non-European +/// [`GreeksError`] surfaced by `numerical_theta` for non-European /// options. pub fn theta(option: &Options) -> Result { let t = option.expiration_date.get_years()?; @@ -932,7 +932,7 @@ pub fn theta(option: &Options) -> Result { /// /// Returns [`GreeksError::ExpirationDate`] when the option's expiration /// cannot be converted to a positive year fraction, and propagates any -/// [`GreeksError`] surfaced by [`numerical_vega`] for non-European +/// [`GreeksError`] surfaced by `numerical_vega` for non-European /// options. pub fn vega(option: &Options) -> Result { let expiration_date: Positive = option.expiration_date.get_years()?; @@ -1063,7 +1063,7 @@ pub fn vega(option: &Options) -> Result { /// /// Returns [`GreeksError::ExpirationDate`] when the option's expiration /// cannot be converted to a positive year fraction, and propagates any -/// [`GreeksError`] surfaced by [`numerical_rho`] for non-European +/// [`GreeksError`] surfaced by `numerical_rho` for non-European /// options. pub fn rho(option: &Options) -> Result { // Get time to expiration first and validate diff --git a/src/greeks/numerical.rs b/src/greeks/numerical.rs index cc6f2da7..81dd9f81 100644 --- a/src/greeks/numerical.rs +++ b/src/greeks/numerical.rs @@ -25,10 +25,10 @@ const H: Decimal = dec!(0.01); /// /// # Errors /// -/// Propagates any [`PricingError`] returned by the unified-pricing +/// Propagates any `PricingError` returned by the unified-pricing /// evaluator on the perturbed option clones, wrapped as /// [`GreeksError::Pricing`]; typically -/// [`PricingError::ExpirationDate`] or [`PricingError::MethodError`] on +/// `PricingError::ExpirationDate` or `PricingError::MethodError` on /// numerical failure. pub fn numerical_delta(option: &Options) -> Result { let mut opt_plus = option.clone(); @@ -52,7 +52,7 @@ pub fn numerical_delta(option: &Options) -> Result { /// /// # Errors /// -/// Propagates any [`PricingError`] returned by the unified-pricing +/// Propagates any `PricingError` returned by the unified-pricing /// evaluator on the three perturbed option clones, wrapped as /// [`GreeksError::Pricing`]. pub fn numerical_gamma(option: &Options) -> Result { @@ -78,7 +78,7 @@ pub fn numerical_gamma(option: &Options) -> Result { /// /// # Errors /// -/// Propagates any [`PricingError`] returned by the unified-pricing +/// Propagates any `PricingError` returned by the unified-pricing /// evaluator on the perturbed option clones, wrapped as /// [`GreeksError::Pricing`]. pub fn numerical_vega(option: &Options) -> Result { @@ -103,7 +103,7 @@ pub fn numerical_vega(option: &Options) -> Result { /// # Errors /// /// Returns [`GreeksError::ExpirationDate`] when the option's expiration -/// cannot be resolved, and propagates any [`PricingError`] returned by +/// cannot be resolved, and propagates any `PricingError` returned by /// the unified-pricing evaluator on the perturbed option clones /// (wrapped as [`GreeksError::Pricing`]). pub fn numerical_theta(option: &Options) -> Result { @@ -127,7 +127,7 @@ pub fn numerical_theta(option: &Options) -> Result { /// /// # Errors /// -/// Propagates any [`PricingError`] returned by the unified-pricing +/// Propagates any `PricingError` returned by the unified-pricing /// evaluator on the perturbed option clones, wrapped as /// [`GreeksError::Pricing`]. pub fn numerical_rho(option: &Options) -> Result { diff --git a/src/metrics/temporal/charm.rs b/src/metrics/temporal/charm.rs index 7bd099ce..5433745d 100644 --- a/src/metrics/temporal/charm.rs +++ b/src/metrics/temporal/charm.rs @@ -130,7 +130,7 @@ pub trait CharmSurface { /// Returns [`SurfaceError::ConstructionError`] when the sampling /// grid is empty, and propagates [`SurfaceError::Point3DError`] or /// [`SurfaceError::OperationError`] from the intermediate - /// [`charm`] evaluation at each `(price, days)` sample. + /// `charm` evaluation at each `(price, days)` sample. fn charm_surface( &self, price_range: (Positive, Positive), diff --git a/src/metrics/temporal/color.rs b/src/metrics/temporal/color.rs index 27ea9442..790079ea 100644 --- a/src/metrics/temporal/color.rs +++ b/src/metrics/temporal/color.rs @@ -130,7 +130,7 @@ pub trait ColorSurface { /// Returns [`SurfaceError::ConstructionError`] when the sampling /// grid is empty, and propagates [`SurfaceError::Point3DError`] or /// [`SurfaceError::OperationError`] from the intermediate - /// [`color`] evaluation at each `(price, days)` sample. + /// `color` evaluation at each `(price, days)` sample. fn color_surface( &self, price_range: (Positive, Positive), diff --git a/src/metrics/temporal/theta.rs b/src/metrics/temporal/theta.rs index 8e492904..c238e784 100644 --- a/src/metrics/temporal/theta.rs +++ b/src/metrics/temporal/theta.rs @@ -140,7 +140,7 @@ pub trait ThetaSurface { /// Returns [`SurfaceError::ConstructionError`] when the sampling /// grid is empty, and propagates [`SurfaceError::Point3DError`] or /// [`SurfaceError::OperationError`] from the intermediate - /// [`theta`] evaluation at each `(price, days)` sample. + /// `theta` evaluation at each `(price, days)` sample. fn theta_surface( &self, price_range: (Positive, Positive), diff --git a/src/model/option.rs b/src/model/option.rs index a7a7e222..9472534f 100644 --- a/src/model/option.rs +++ b/src/model/option.rs @@ -366,7 +366,7 @@ impl Options { /// /// Returns [`OptionsError::ExpirationDate`] when the option's expiration /// cannot be converted to a positive year fraction, or propagates any - /// [`PricingError`] surfaced by [`generate_binomial_tree`] (e.g. + /// `PricingError` surfaced by [`generate_binomial_tree`] (e.g. /// [`PricingError::BinomialNodeMissing`] or [`PricingError::SqrtFailure`]). pub fn calculate_price_binomial_tree(&self, no_steps: usize) -> PriceBinomialTree { let expiry = self.time_to_expiration()?; @@ -405,10 +405,10 @@ impl Options { /// /// # Errors /// - /// Propagates any [`PricingError`] returned by [`black_scholes`] (wrapped as - /// [`OptionsError::PricingError`]), most commonly - /// [`PricingError::ExpirationDate`] when the expiration cannot be resolved - /// or [`PricingError::MethodError`] when the closed-form formula fails + /// Propagates any `PricingError` returned by [`black_scholes`] (wrapped as + /// `OptionsError::PricingError`), most commonly + /// `PricingError::ExpirationDate` when the expiration cannot be resolved + /// or `PricingError::MethodError` when the closed-form formula fails /// numerically. pub fn calculate_price_black_scholes(&self) -> OptionsResult { Ok(black_scholes(self)?) @@ -454,9 +454,9 @@ impl Options { /// /// # Errors /// - /// Propagates any [`PricingError`] returned by [`telegraph`] (wrapped as - /// [`OptionsError::PricingError`]), typically - /// [`PricingError::ExpirationDate`] or [`PricingError::MethodError`] + /// Propagates any `PricingError` returned by the `telegraph` pricing + /// kernel (wrapped as `OptionsError::PricingError`), typically + /// `PricingError::ExpirationDate` or `PricingError::MethodError` /// when the finite-difference kernel fails to converge. pub fn calculate_price_telegraph(&self, no_steps: usize) -> OptionsResult { Ok(telegraph(self, no_steps, None, None)?) @@ -482,7 +482,7 @@ impl Options { /// Currently infallible in practice (the `f64` → `Decimal` conversion falls /// back to `Decimal::default()`), but the `Result` signature is retained /// for forward-compatibility with exotic payoff kernels that may surface - /// [`OptionsError`] variants. + /// `OptionsError` variants. pub fn payoff(&self) -> OptionsResult { let payoff_info = PayoffInfo { spot: self.underlying_price, @@ -517,7 +517,7 @@ impl Options { /// Currently infallible in practice (the `f64` → `Decimal` conversion falls /// back to `Decimal::default()`), but the `Result` signature is retained /// for forward-compatibility with exotic payoff kernels that may surface - /// [`OptionsError`] variants. + /// `OptionsError` variants. pub fn payoff_at_price(&self, price: &Positive) -> OptionsResult { let payoff_info = PayoffInfo { spot: *price, @@ -551,7 +551,7 @@ impl Options { /// Currently infallible in practice (the `f64` → `Decimal` conversion falls /// back to `Decimal::default()`), but the `Result` signature is retained /// for forward-compatibility with exotic payoff kernels that may surface - /// [`OptionsError`] variants. + /// `OptionsError` variants. pub fn intrinsic_value(&self, underlying_price: Positive) -> OptionsResult { let payoff_info = PayoffInfo { spot: underlying_price, @@ -598,10 +598,10 @@ impl Options { /// /// # Errors /// - /// Propagates any [`OptionsError`] returned by + /// Propagates any `OptionsError` returned by /// [`Options::calculate_price_black_scholes`] or - /// [`Options::intrinsic_value`] (typically [`OptionsError::PricingError`] - /// with [`PricingError::ExpirationDate`] as the inner cause). + /// [`Options::intrinsic_value`] (typically `OptionsError::PricingError` + /// with `PricingError::ExpirationDate` as the inner cause). pub fn time_value(&self) -> OptionsResult { let option_price = self.calculate_price_black_scholes()?.abs(); let intrinsic_value = self.intrinsic_value(self.underlying_price)?; @@ -729,7 +729,7 @@ impl Options { /// Returns [`VolatilityError::PositiveError`] when the midpoint /// volatility breaches the `Positive` invariant, /// [`VolatilityError::NoConvergence`] when the bisection exhausts - /// [`MAX_ITERATIONS_IV`] without matching the target price, or propagates + /// `MAX_ITERATIONS_IV` without matching the target price, or propagates /// [`VolatilityError::Options`] from the underlying Black–Scholes /// evaluation (wrapped as [`OptionsError::ImpliedVolatilityInvariant`] /// when the invariant check fails). diff --git a/src/model/position.rs b/src/model/position.rs index e96d204f..942e2b01 100644 --- a/src/model/position.rs +++ b/src/model/position.rs @@ -404,9 +404,9 @@ impl Position { /// /// # Errors /// - /// Propagates any [`OptionsError`] returned by the underlying payoff + /// Propagates any `OptionsError` returned by the underlying payoff /// evaluation ([`Options::intrinsic_value`] or [`Options::payoff`]), - /// wrapped as [`PricingError::OptionError`]. + /// wrapped as `PricingError::OptionError`. pub fn pnl_at_expiration(&self, price: &Option<&Positive>) -> Result { match price { None => Ok(self.option.intrinsic_value(self.option.underlying_price)? @@ -467,7 +467,7 @@ impl Position { /// /// Returns [`PositionError`] wrapping any /// [`PositionValidationErrorKind`] surfaced by the internal Black–Scholes - /// evaluation, or [`PositionError::PricingError`] when the + /// evaluation, or `PositionError::PricingError` when the /// implied-volatility recomputation at `price` fails. pub fn unrealized_pnl(&self, price: Positive) -> Result { match self.option.side { @@ -500,7 +500,7 @@ impl Position { /// # Errors /// /// Returns [`PositionError`] wrapping a - /// [`PositionValidationErrorKind::InvalidPositionSize`] if the elapsed + /// `PositionValidationErrorKind::InvalidPositionSize` if the elapsed /// day-count is negative (future-dated open date) or cannot be /// represented as a `Positive`. pub fn days_held(&self) -> Result { diff --git a/src/model/profit_range.rs b/src/model/profit_range.rs index ed2958ea..68b322be 100644 --- a/src/model/profit_range.rs +++ b/src/model/profit_range.rs @@ -47,8 +47,8 @@ impl ProfitLossRange { /// # Errors /// /// Returns [`ProbabilityError`] wrapping a - /// [`ProbabilityCalculationErrorKind::InvalidProbability`] when `lower_bound` - /// is greater than `upper_bound`, or [`ProbabilityCalculationErrorKind::InvalidProbabilityRange`] + /// `ProbabilityCalculationErrorKind::InvalidProbability` when `lower_bound` + /// is greater than `upper_bound`, or `ProbabilityCalculationErrorKind::InvalidProbabilityRange` /// when the `probability` value lies outside the closed interval `[0, 1]`. pub fn new( lower_bound: Option, diff --git a/src/pnl/traits.rs b/src/pnl/traits.rs index 4eb2bcc9..58f38201 100644 --- a/src/pnl/traits.rs +++ b/src/pnl/traits.rs @@ -30,10 +30,10 @@ pub trait PnLCalculator { /// /// # Errors /// - /// Returns [`PricingError::ExpirationDate`] when the expiration - /// cannot be converted, [`PricingError::MethodError`] when the + /// Returns `PricingError::ExpirationDate` when the expiration + /// cannot be converted, `PricingError::MethodError` when the /// underlying Black–Scholes kernel fails, or propagates any other - /// [`PricingError`] surfaced by the strategy's component + /// `PricingError` surfaced by the strategy's component /// evaluations. fn calculate_pnl( &self, @@ -56,9 +56,9 @@ pub trait PnLCalculator { /// /// # Errors /// - /// Returns [`PricingError::MethodError`] when the terminal + /// Returns `PricingError::MethodError` when the terminal /// payoff evaluation fails on any leg (typically propagated from - /// [`Position::pnl_at_expiration`]). + /// `Position::pnl_at_expiration`). fn calculate_pnl_at_expiration( &self, _underlying_price: &Positive, diff --git a/src/pricing/american.rs b/src/pricing/american.rs index 4e3d41dc..319bd3c6 100644 --- a/src/pricing/american.rs +++ b/src/pricing/american.rs @@ -122,10 +122,10 @@ const TOLERANCE: f64 = 1e-6; /// /// # Errors /// -/// Returns [`PricingError::MethodError`] (with method +/// Returns `PricingError::MethodError` (with method /// `barone_adesi_whaley`) when the intermediate Newton–Raphson /// iteration cannot converge on the early-exercise boundary -/// [`Sx`], and [`PricingError::SqrtFailure`] when the quadratic +/// `Sx`, and [`PricingError::SqrtFailure`] when the quadratic /// formula receives a negative discriminant from the approximation /// parameters. pub fn barone_adesi_whaley( diff --git a/src/pricing/barrier.rs b/src/pricing/barrier.rs index 9764feb0..28956800 100644 --- a/src/pricing/barrier.rs +++ b/src/pricing/barrier.rs @@ -19,9 +19,9 @@ use rust_decimal_macros::dec; /// /// Returns [`PricingError::UnsupportedOptionType`] when `option` /// is not a [`OptionType::Barrier`] variant, and propagates any -/// [`PricingError`] raised by the underlying Black\u2013Scholes closed +/// `PricingError` raised by the underlying Black\u2013Scholes closed /// form on the decomposed vanilla components (typically -/// [`PricingError::ExpirationDate`] or [`PricingError::Positive`]). +/// `PricingError::ExpirationDate` or [`PricingError::Positive`]). pub fn barrier_black_scholes(option: &Options) -> Result { let (barrier_type, barrier_level, rebate) = match &option.option_type { OptionType::Barrier { diff --git a/src/pricing/binary.rs b/src/pricing/binary.rs index 7684542d..beafde68 100644 --- a/src/pricing/binary.rs +++ b/src/pricing/binary.rs @@ -54,8 +54,8 @@ const DEFAULT_CASH_PAYOUT: Decimal = dec!(1.0); /// /// Returns [`PricingError::UnsupportedOptionType`] when `option` is /// not an [`OptionType::Binary`] variant, and propagates any -/// [`PricingError`] raised by intermediate Black–Scholes kernels -/// (typically [`PricingError::ExpirationDate`] or +/// `PricingError` raised by intermediate Black–Scholes kernels +/// (typically `PricingError::ExpirationDate` or /// [`PricingError::Positive`]). pub fn binary_black_scholes(option: &Options) -> Result { match &option.option_type { diff --git a/src/pricing/black_scholes_model.rs b/src/pricing/black_scholes_model.rs index 8e1f93e5..e20dcd4f 100644 --- a/src/pricing/black_scholes_model.rs +++ b/src/pricing/black_scholes_model.rs @@ -59,9 +59,9 @@ use tracing::trace; /// /// # Errors /// -/// Returns [`PricingError::ExpirationDate`] when the expiration +/// Returns `PricingError::ExpirationDate` when the expiration /// cannot be converted to a positive year fraction, -/// [`PricingError::MethodError`] when the `d1`/`d2` evaluation hits a +/// `PricingError::MethodError` when the `d1`/`d2` evaluation hits a /// numerical wall (e.g. zero volatility or non-finite intermediate /// value), and [`PricingError::UnsupportedOptionType`] for exotic /// option types not handled by the closed form. @@ -293,7 +293,7 @@ pub trait BlackScholes { /// /// # Errors /// - /// Returns [`PricingError::MethodError`] when the implementor + /// Returns `PricingError::MethodError` when the implementor /// cannot resolve the current option (e.g. a placeholder wrapper /// before the option has been bound to a trade or position). fn get_option(&self) -> Result<&Options, PricingError>; @@ -310,10 +310,10 @@ pub trait BlackScholes { /// /// # Errors /// - /// Propagates any [`PricingError`] returned by - /// [`BlackScholesOption::get_option`] or by [`black_scholes`] - /// (typically [`PricingError::ExpirationDate`] or - /// [`PricingError::MethodError`]). + /// Propagates any `PricingError` returned by + /// `BlackScholesOption::get_option` or by [`black_scholes`] + /// (typically `PricingError::ExpirationDate` or + /// `PricingError::MethodError`). fn calculate_price_black_scholes(&self) -> Result { let option = self.get_option()?; black_scholes(option) diff --git a/src/pricing/chooser.rs b/src/pricing/chooser.rs index 00397ee6..1df8ba47 100644 --- a/src/pricing/chooser.rs +++ b/src/pricing/chooser.rs @@ -46,7 +46,7 @@ use rust_decimal_macros::dec; /// /// Returns [`PricingError::UnsupportedOptionType`] when `option` is /// not an [`OptionType::Chooser`] variant, and propagates any -/// [`PricingError`] raised by the Black–Scholes evaluation at the +/// `PricingError` raised by the Black–Scholes evaluation at the /// chooser date (call and put side). pub fn chooser_black_scholes(option: &Options) -> Result { match &option.option_type { diff --git a/src/pricing/cliquet.rs b/src/pricing/cliquet.rs index a508c926..c715f333 100644 --- a/src/pricing/cliquet.rs +++ b/src/pricing/cliquet.rs @@ -44,7 +44,7 @@ use rust_decimal_macros::dec; /// /// Returns [`PricingError::UnsupportedOptionType`] when `option` is /// not an [`OptionType::Cliquet`] variant, and propagates any -/// [`PricingError`] raised by intermediate Black–Scholes kernels on +/// `PricingError` raised by intermediate Black–Scholes kernels on /// the per-reset forward components. pub fn cliquet_black_scholes(option: &Options) -> Result { match &option.option_type { diff --git a/src/pricing/compound.rs b/src/pricing/compound.rs index 86636633..ee2291b4 100644 --- a/src/pricing/compound.rs +++ b/src/pricing/compound.rs @@ -212,7 +212,7 @@ fn standard_normal_cdf(x: f64) -> f64 { /// /// Returns [`PricingError::UnsupportedOptionType`] when `option` is /// not an [`OptionType::Compound`] variant, and propagates any -/// [`PricingError`] raised by the Black–Scholes evaluation of the +/// `PricingError` raised by the Black–Scholes evaluation of the /// outer option on the inner-option implied value. pub fn compound_black_scholes(option: &Options) -> Result { match &option.option_type { diff --git a/src/pricing/lookback.rs b/src/pricing/lookback.rs index 3c0e229e..5c722152 100644 --- a/src/pricing/lookback.rs +++ b/src/pricing/lookback.rs @@ -47,7 +47,7 @@ use rust_decimal_macros::dec; /// /// Returns [`PricingError::UnsupportedOptionType`] when `option` is /// not an [`OptionType::Lookback`] variant, and propagates any -/// [`PricingError`] raised by intermediate Black–Scholes kernels +/// `PricingError` raised by intermediate Black–Scholes kernels /// on the running extremum decomposition. pub fn lookback_black_scholes(option: &Options) -> Result { match &option.option_type { diff --git a/src/pricing/monte_carlo.rs b/src/pricing/monte_carlo.rs index 43857780..50f8cab1 100644 --- a/src/pricing/monte_carlo.rs +++ b/src/pricing/monte_carlo.rs @@ -35,9 +35,9 @@ use rust_decimal::{Decimal, MathematicalOps}; /// /// # Errors /// -/// Returns [`PricingError::ExpirationDate`] when the option's +/// Returns `PricingError::ExpirationDate` when the option's /// expiration cannot be converted to a positive year fraction, and -/// [`PricingError::MethodError`] when the GBM discretisation +/// `PricingError::MethodError` when the GBM discretisation /// encounters a non-finite value (e.g. volatility overflow) or when /// the terminal payoff averaging produces a non-representable /// `Decimal`. diff --git a/src/pricing/payoff.rs b/src/pricing/payoff.rs index 2fdf86c4..e3f34129 100644 --- a/src/pricing/payoff.rs +++ b/src/pricing/payoff.rs @@ -229,9 +229,9 @@ pub trait Profit { /// /// # Errors /// - /// Returns [`PricingError::MethodError`] when the strategy cannot + /// Returns `PricingError::MethodError` when the strategy cannot /// evaluate its payoff at `price` (typically propagated from the - /// underlying [`Position::pnl_at_expiration`] or Black–Scholes + /// underlying `Position::pnl_at_expiration` or Black–Scholes /// evaluation). fn calculate_profit_at(&self, price: &Positive) -> Result; @@ -251,7 +251,7 @@ pub trait Profit { /// /// # Errors /// - /// Propagates any [`PricingError`] returned by + /// Propagates any `PricingError` returned by /// [`Profit::calculate_profit_at`]. fn get_point_at_price(&self, _price: &Positive) -> Result<(Decimal, Decimal), PricingError> { let profit = self.calculate_profit_at(_price)?; diff --git a/src/pricing/telegraph.rs b/src/pricing/telegraph.rs index 5c2a7ba0..a4ad6e80 100644 --- a/src/pricing/telegraph.rs +++ b/src/pricing/telegraph.rs @@ -319,8 +319,8 @@ pub(crate) fn estimate_telegraph_parameters( /// /// # Errors /// -/// Returns [`PricingError::ExpirationDate`] when the option's -/// expiration cannot be converted, [`PricingError::MethodError`] +/// Returns `PricingError::ExpirationDate` when the option's +/// expiration cannot be converted, `PricingError::MethodError` /// when the finite-difference recurrence fails to populate a node /// (e.g. parameter estimation produces degenerate rates) or when the /// terminal averaging yields a non-finite value. diff --git a/src/pricing/unified.rs b/src/pricing/unified.rs index 70c0cdaf..146ed17a 100644 --- a/src/pricing/unified.rs +++ b/src/pricing/unified.rs @@ -71,8 +71,8 @@ pub enum PricingEngine { /// /// # Errors /// -/// Propagates any [`PricingError`] returned by the selected engine: -/// [`PricingError::ExpirationDate`] or [`PricingError::MethodError`] +/// Propagates any `PricingError` returned by the selected engine: +/// `PricingError::ExpirationDate` or `PricingError::MethodError` /// from Black–Scholes, [`PricingError::BinomialNodeMissing`] or /// [`PricingError::SqrtFailure`] from the binomial lattice, or the /// equivalent failures from exotic engines (barrier, binary, @@ -108,7 +108,7 @@ pub trait Priceable { /// /// # Errors /// - /// Propagates any [`PricingError`] returned by the selected + /// Propagates any `PricingError` returned by the selected /// engine; see [`price_option`] for the full variant breakdown. fn price(&self, engine: &PricingEngine) -> PricingResult; } diff --git a/src/strategies/base.rs b/src/strategies/base.rs index b8b00fda..c428be3e 100644 --- a/src/strategies/base.rs +++ b/src/strategies/base.rs @@ -841,7 +841,7 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// # Errors /// /// Propagates any [`StrategyError`] returned by - /// [`Strategable::get_max_profit`] on `&self`. + /// `Strategable::get_max_profit` on `&self`. fn get_max_profit_mut(&mut self) -> Result { self.get_max_profit() } @@ -876,7 +876,7 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// # Errors /// /// Propagates any [`StrategyError`] returned by - /// [`Strategable::get_max_loss`] on `&self`. + /// `Strategable::get_max_loss` on `&self`. fn get_max_loss_mut(&mut self) -> Result { self.get_max_loss() } @@ -890,7 +890,7 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// # Errors /// /// Propagates any [`PositionError`] returned by - /// [`Strategable::get_positions`] or by + /// `Strategable::get_positions` or by /// [`Position::total_cost`] when the component legs surface invalid /// state. fn get_total_cost(&self) -> Result { @@ -912,7 +912,7 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// # Errors /// /// Propagates any [`PositionError`] returned by - /// [`Strategable::get_positions`] or by + /// `Strategable::get_positions` or by /// [`Position::net_cost`] when the component legs surface invalid /// state. fn get_net_cost(&self) -> Result { @@ -933,8 +933,8 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// /// # Errors /// - /// Returns [`StrategyError::from(PositionError)`] when - /// [`Strategable::get_positions`] or + /// Returns `StrategyError::from(PositionError)` when + /// `Strategable::get_positions` or /// [`Position::net_premium_received`] fail on any leg. fn get_net_premium_received(&self) -> Result { let positions = self.get_positions()?; @@ -961,8 +961,8 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// /// # Errors /// - /// Returns [`StrategyError::from(PositionError)`] when - /// [`Strategable::get_positions`] or [`Position::fees`] fail on any + /// Returns `StrategyError::from(PositionError)` when + /// `Strategable::get_positions` or [`Position::fees`] fail on any /// leg. fn get_fees(&self) -> Result { let mut fee = Positive::ZERO; @@ -1035,8 +1035,8 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// # Errors /// /// Propagates any [`StrategyError`] returned by - /// [`Strategable::get_break_even_points`] or - /// [`Strategable::get_max_min_strikes`]. + /// `Strategable::get_break_even_points` or + /// `Strategable::get_max_min_strikes`. fn get_range_to_show(&self) -> Result<(Positive, Positive), StrategyError> { let mut all_points = self.get_break_even_points()?.clone(); let (first_strike, last_strike) = self.get_max_min_strikes()?; @@ -1079,7 +1079,7 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// # Errors /// /// Propagates any [`StrategyError`] returned by - /// [`Strategable::get_range_to_show`]. + /// `Strategable::get_range_to_show`. fn get_best_range_to_show(&self, step: Positive) -> Result, StrategyError> { let (start_price, end_price) = self.get_range_to_show()?; Ok(calculate_price_range(start_price, end_price, step)) @@ -1096,7 +1096,7 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// /// Returns [`StrategyError::PriceError`] when the strategy has no /// strikes to compare against; propagates [`StrategyError`] variants - /// from [`Strategable::get_positions`] when position enumeration fails. + /// from `Strategable::get_positions` when position enumeration fails. fn get_max_min_strikes(&self) -> Result<(Positive, Positive), StrategyError> { let strikes: Vec<&Positive> = self.get_strikes(); if strikes.is_empty() { @@ -1142,7 +1142,7 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// /// Returns [`StrategyError::BreakEvenError`] when the strategy has /// no break-even points, and propagates any other [`StrategyError`] - /// surfaced by [`Strategable::get_break_even_points`]. + /// surfaced by `Strategable::get_break_even_points`. fn get_range_of_profit(&self) -> Result { let mut break_even_points = self.get_break_even_points()?.clone(); match break_even_points.len() { diff --git a/src/strategies/build/traits.rs b/src/strategies/build/traits.rs index 08f57ce5..b7250227 100644 --- a/src/strategies/build/traits.rs +++ b/src/strategies/build/traits.rs @@ -44,7 +44,7 @@ pub trait StrategyConstructor: Strategies + Greeks { /// /// # Errors /// - /// Returns [`StrategyError::StrategyInvalid`] when the provided + /// Returns `StrategyError::StrategyInvalid` when the provided /// positions do not match the leg count or side/style pattern /// required by the implementing strategy type. fn get_strategy(_vec_positions: &[Position]) -> Result diff --git a/src/strategies/collar.rs b/src/strategies/collar.rs index 7091e6a7..16adcc14 100644 --- a/src/strategies/collar.rs +++ b/src/strategies/collar.rs @@ -378,10 +378,10 @@ impl Collar { /// /// # Errors /// - /// Returns [`PricingError::MethodError`] with method `collar` when + /// Returns `PricingError::MethodError` with method `collar` when /// the short-call strike is below the spot cost basis (the strategy /// is not economically well-formed). Arithmetic follows - /// [`PricingError::ArithmeticFailure`] if fee or premium conversions + /// `PricingError::ArithmeticFailure` if fee or premium conversions /// overflow. pub fn max_profit_potential(&self) -> Result { let call_strike = self.call_strike(); @@ -408,7 +408,7 @@ impl Collar { /// /// # Errors /// - /// Returns [`PricingError::MethodError`] with method `collar` when + /// Returns `PricingError::MethodError` with method `collar` when /// the loss decomposition underflows (for example when the put strike /// equals or exceeds the spot cost basis and the net premium net of /// fees is positive), or when arithmetic fails while converting diff --git a/src/strategies/covered_call.rs b/src/strategies/covered_call.rs index a25ed3de..d9ba8636 100644 --- a/src/strategies/covered_call.rs +++ b/src/strategies/covered_call.rs @@ -294,7 +294,7 @@ impl CoveredCall { /// /// # Errors /// - /// Returns [`PricingError::MethodError`] with method `covered_call` + /// Returns `PricingError::MethodError` with method `covered_call` /// when the call strike sits below the spot cost basis (the strategy /// has no upside), or when arithmetic on `Positive` operands /// overflows. @@ -327,7 +327,7 @@ impl CoveredCall { /// /// # Errors /// - /// Returns [`PricingError::MethodError`] with method `covered_call` + /// Returns `PricingError::MethodError` with method `covered_call` /// when the premium net of fees exceeds `cost_basis × quantity` /// (which would imply a non-negative worst case), or when arithmetic /// on `Positive` operands overflows. diff --git a/src/strategies/delta_neutral/model.rs b/src/strategies/delta_neutral/model.rs index fdd62ae2..4cdc68fc 100644 --- a/src/strategies/delta_neutral/model.rs +++ b/src/strategies/delta_neutral/model.rs @@ -353,7 +353,7 @@ pub trait DeltaNeutrality: Greeks + Positionable + Strategies { /// # Errors /// /// Propagates any [`GreeksError`] surfaced by - /// [`Strategable::get_options`] or by the per-option + /// `Strategable::get_options` or by the per-option /// [`Greeks::delta`] evaluation, and returns /// [`GreeksError::DeltaNeutrality`] with /// [`DeltaNeutralityErrorKind::NotAchievable`] when the portfolio @@ -607,7 +607,7 @@ pub trait DeltaNeutrality: Greeks + Positionable + Strategies { /// /// Propagates any [`GreeksError`] returned by /// [`Greeks::delta`] on individual legs, or by the internal - /// [`delta_neutrality`] computation. + /// `delta_neutrality` computation. fn delta_adjustments(&self) -> Result, GreeksError> { let net_delta = self.delta()?; @@ -740,9 +740,9 @@ pub trait DeltaNeutrality: Greeks + Positionable + Strategies { /// # Errors /// /// Returns [`StrategyError::GreeksError`] when the underlying - /// [`delta_neutrality`] or [`delta_adjustments`] calls fail, and - /// [`StrategyError::PositionError`] (via - /// [`apply_single_adjustment`]) when the adjustment cannot be + /// `delta_neutrality` or `delta_adjustments` calls fail, and + /// `StrategyError::PositionError` (via + /// `apply_single_adjustment`) when the adjustment cannot be /// committed to the strategy. fn apply_delta_adjustments(&mut self, action: Option) -> Result<(), StrategyError> { let delta_info = self.delta_neutrality()?; @@ -826,8 +826,8 @@ pub trait DeltaNeutrality: Greeks + Positionable + Strategies { /// /// # Errors /// - /// Returns [`StrategyError::PositionError`] when the underlying - /// [`adjust_option_position`] rejects the requested quantity change + /// Returns `StrategyError::PositionError` when the underlying + /// `adjust_option_position` rejects the requested quantity change /// (e.g. strike not found, invalid side for the adjustment, or /// invariant violation on the resulting position). fn apply_single_adjustment( @@ -925,8 +925,8 @@ pub trait DeltaNeutrality: Greeks + Positionable + Strategies { /// # Errors /// /// Returns [`StrategyError::GreeksError`] when the internal - /// [`delta_adjustments`] call fails, and - /// [`StrategyError::PositionError`] when the adjustment cannot be + /// `delta_adjustments` call fails, and + /// `StrategyError::PositionError` when the adjustment cannot be /// converted into a committable trade (invalid side, strike not /// present in the chain, or quantity invariant breach). fn trade_from_delta_adjustment(&mut self, action: Action) -> Result, StrategyError> { @@ -1033,7 +1033,7 @@ pub trait DeltaNeutrality: Greeks + Positionable + Strategies { /// # Errors /// /// Propagates any [`GreeksError`] surfaced by - /// [`Strategable::get_positions`] or by + /// `Strategable::get_positions` or by /// [`PortfolioGreeks::from_positions`], typically /// [`GreeksError::Pricing`] when individual Black–Scholes legs fail. fn portfolio_greeks(&self) -> Result { @@ -1080,8 +1080,8 @@ pub trait DeltaNeutrality: Greeks + Positionable + Strategies { /// # Errors /// /// Returns [`StrategyError::GreeksError`] when the portfolio Greeks - /// cannot be computed, or [`StrategyError::Adjustment`] (mapped from - /// [`AdjustmentError`]) when the optimiser cannot find a viable plan + /// cannot be computed, or `StrategyError::Adjustment` (mapped from + /// `AdjustmentError`) when the optimiser cannot find a viable plan /// for the provided `config` and `target` (e.g. no positions, /// infeasible constraints, or cost ceiling breached). fn optimized_adjustment_plan( @@ -1118,9 +1118,9 @@ pub trait DeltaNeutrality: Greeks + Positionable + Strategies { /// # Errors /// /// Same failure surface as - /// [`optimized_adjustment_plan`] plus additional chain-driven - /// failures: returns [`StrategyError::Adjustment`] mapped from - /// [`AdjustmentError`] when the chain does not expose any candidate + /// `optimized_adjustment_plan` plus additional chain-driven + /// failures: returns `StrategyError::Adjustment` mapped from + /// `AdjustmentError` when the chain does not expose any candidate /// strike that satisfies the target delta direction. fn optimized_adjustment_plan_with_chain( &self, diff --git a/src/strategies/delta_neutral/optimizer.rs b/src/strategies/delta_neutral/optimizer.rs index 103a7b0c..03deeb79 100644 --- a/src/strategies/delta_neutral/optimizer.rs +++ b/src/strategies/delta_neutral/optimizer.rs @@ -104,9 +104,9 @@ impl<'a> AdjustmentOptimizer<'a> { /// # Errors /// /// Returns [`AdjustmentError::NoPositions`] when `positions` is - /// empty, [`AdjustmentError::InfeasibleTarget`] when no combination + /// empty, `AdjustmentError::InfeasibleTarget` when no combination /// of candidate strikes can close the delta gap within the - /// configured tolerance, or [`AdjustmentError::CostCeilingBreached`] + /// configured tolerance, or `AdjustmentError::CostCeilingBreached` /// when every viable plan exceeds the configured `max_cost`. pub fn optimize(&self) -> Result { if self.positions.is_empty() { diff --git a/src/strategies/probabilities/core.rs b/src/strategies/probabilities/core.rs index c1628960..f637d46b 100644 --- a/src/strategies/probabilities/core.rs +++ b/src/strategies/probabilities/core.rs @@ -70,8 +70,8 @@ pub trait ProbabilityAnalysis: Strategies + Profit { /// # Errors /// /// Propagates any [`ProbabilityError`] returned by - /// [`probability_of_profit`], [`probability_of_loss`], - /// [`calculate_extreme_probabilities`] or [`expected_value`]; + /// `probability_of_profit`, `probability_of_loss`, + /// `calculate_extreme_probabilities` or `expected_value`; /// typically [`ProbabilityError::CalculationError`] when an /// underlying break-even or payoff evaluation fails. fn analyze_probabilities( @@ -146,8 +146,8 @@ pub trait ProbabilityAnalysis: Strategies + Profit { /// Returns [`ProbabilityError::CalculationError`] when the display /// range cannot be computed or when profit evaluation fails at a /// sample point, and propagates any [`ProbabilityError`] surfaced by - /// [`probability_at`] (typically - /// [`ProbabilityCalculationErrorKind::InvalidProbabilityRange`] + /// `probability_at` (typically + /// `ProbabilityCalculationErrorKind::InvalidProbabilityRange` /// for malformed volatility adjustments). fn expected_value( &self, @@ -321,7 +321,7 @@ pub trait ProbabilityAnalysis: Strategies + Profit { /// # Errors /// /// Propagates any [`ProbabilityError`] returned by - /// [`probability_of_profit`] and [`probability_of_loss`]; typically + /// `probability_of_profit` and `probability_of_loss`; typically /// [`ProbabilityError::CalculationError`] when a tail integration /// beyond the break-even points fails. fn calculate_extreme_probabilities( diff --git a/src/strategies/protective_put.rs b/src/strategies/protective_put.rs index 4fb97736..b014e67a 100644 --- a/src/strategies/protective_put.rs +++ b/src/strategies/protective_put.rs @@ -184,7 +184,7 @@ impl ProtectivePut { /// /// # Errors /// - /// Returns [`PricingError::MethodError`] with method + /// Returns `PricingError::MethodError` with method /// `protective_put` when the put strike equals or exceeds the spot /// cost basis (the hedge fully neutralises downside and the /// decomposition is ill-posed), or when arithmetic on `Positive` diff --git a/src/surfaces/traits.rs b/src/surfaces/traits.rs index 11ad8594..a08fae4e 100644 --- a/src/surfaces/traits.rs +++ b/src/surfaces/traits.rs @@ -52,6 +52,6 @@ pub trait Surfacable { /// implementor cannot produce any `(x, y, z)` sample /// (empty data source, degenerate axes, or missing metric), and /// propagates [`SurfaceError::Point3DError`] when a sample - /// cannot be represented as a [`Point3D`]. + /// cannot be represented as a `Point3D`. fn surface(&self) -> Result; } diff --git a/src/visualization/plotly.rs b/src/visualization/plotly.rs index df3fffca..ec636b8a 100644 --- a/src/visualization/plotly.rs +++ b/src/visualization/plotly.rs @@ -315,7 +315,7 @@ pub trait Graph { /// Currently infallible (the underlying `plotly` `show` call does /// not return a `Result`); the `Result` signature is retained to /// allow future plot kernels that can surface - /// [`GraphError::RenderError`] or [`GraphError::IoError`] without + /// `GraphError::RenderError` or `GraphError::IoError` without /// a breaking change. #[cfg(feature = "plotly")] fn show(&self) -> Result<(), GraphError> { @@ -327,9 +327,9 @@ pub trait Graph { /// /// # Errors /// - /// Returns [`GraphError::RenderError`] when the chosen + /// Returns `GraphError::RenderError` when the chosen /// [`OutputType`] backend (PNG/SVG via static_export, HTML, etc.) - /// fails to serialize or render, and [`GraphError::IoError`] when + /// fails to serialize or render, and `GraphError::IoError` when /// the destination path cannot be written. #[cfg(feature = "plotly")] fn render(&self, output: OutputType) -> Result<(), GraphError> { @@ -363,8 +363,8 @@ pub trait Graph { /// # Errors /// /// Propagates any [`GraphError`] returned by - /// [`PlotlyChart::write_html`], typically - /// [`GraphError::IoError`] when the target file cannot be + /// `PlotlyChart::write_html`, typically + /// `GraphError::IoError` when the target file cannot be /// created or written. #[cfg(feature = "plotly")] fn to_interactive_html(&self, path: &std::path::Path) -> Result<(), GraphError> { diff --git a/src/volatility/utils.rs b/src/volatility/utils.rs index bb11dce7..2294f96d 100644 --- a/src/volatility/utils.rs +++ b/src/volatility/utils.rs @@ -156,7 +156,7 @@ pub fn ewma_volatility( /// # Errors /// /// Returns [`VolatilityError::NoConvergence`] when the Newton–Raphson -/// iteration exhausts [`MAX_ITERATIONS_IV`] without matching the target +/// iteration exhausts `MAX_ITERATIONS_IV` without matching the target /// price, [`VolatilityError::IvNotFound`] when the search grid never /// produced a valid candidate, or propagates /// [`VolatilityError::Options`] from the underlying Black–Scholes @@ -363,8 +363,8 @@ pub fn simulate_heston_volatility( /// Propagates any [`VolatilityError::Options`] returned by the /// underlying Black–Scholes evaluation at the minimum or maximum /// volatility bound — typically -/// [`OptionsError::PricingError`] with an -/// [`PricingError::ExpirationDate`] inner cause. +/// `OptionsError::PricingError` with an +/// `PricingError::ExpirationDate` inner cause. pub fn uncertain_volatility_bounds( option: &Options, min_volatility: Positive, From 992702178b224bbeaeb0518c7c61c11e57cc0ee1 Mon Sep 17 00:00:00 2001 From: Joaquin Bejar Date: Sun, 19 Apr 2026 14:13:42 +0200 Subject: [PATCH 10/10] address review: align # Errors docs with actual clamping behaviour Copilot review on PR #370 flagged 22 inline comments where the new # Errors sections described error variants that the implementations never actually return (each function clamps intermediate values with unwrap_or(Positive::ZERO) or max(Decimal::ZERO) rather than surfacing an Err). This commit rewrites the affected sections to truthfully describe the current (mostly infallible / clamping) behaviour and preserves the Result signature for forward compatibility. - volatility/utils.rs: drop never-returned PositiveError mentions on constant_volatility, ewma_volatility, simulate_heston_volatility; rewrite implied_volatility docs to match the parallel grid search (not Newton-Raphson); document garch_volatility as currently infallible with a # Panics note for empty input; document historical_volatility as propagating only NumericalFailure with a # Panics note for window_size == 0. - model/position.rs: document total_cost, premium_received and net_premium_received as currently infallible / clamping instead of returning PositionError. - strategies/{collar, covered_call, protective_put}.rs: document max_profit_potential and max_loss_potential as currently infallible clamping functions rather than claiming unreturned PricingError::MethodError variants. - simulation/steps/x.rs: acknowledge that Xstep::previous does an unchecked index decrement (# Panics) rather than returning IndexConversion. - simulation/traits.rs: clarify that brownian's only PositiveError source is the ystep conversion; generated samples are clamped. - surfaces/types.rs: document the positivity checks on Point3D::to_tuple. - visualization/plotly.rs: fix GraphError variant names (Render / Io). - volatility/traits.rs: realign atm_iv Returns and Errors with the actual Result<&Positive, VolatilityError> signature. - error/options.rs: drop the literal HTML-fragment hint. - model/decimal.rs: clarify that decimal_normal_sample's unreachable branch is guaranteed by the statrs contract, not std. - model/profit_range.rs: document the real RangeError variant and note that probability is not currently validated. No production logic changed. cargo clippy --lib -- -W missing_errors_doc -W missing_panics_doc still reports 0 warnings and the strict-warnings doc build stays clean. Refs #334 --- src/error/options.rs | 2 +- src/model/decimal.rs | 10 ++-- src/model/position.rs | 34 ++++++++----- src/model/profit_range.rs | 10 ++-- src/simulation/steps/x.rs | 16 ++++-- src/simulation/traits.rs | 14 ++++-- src/strategies/collar.rs | 26 ++++++---- src/strategies/covered_call.rs | 22 ++++++--- src/strategies/protective_put.rs | 12 +++-- src/surfaces/types.rs | 13 +++-- src/visualization/plotly.rs | 8 +-- src/volatility/traits.rs | 21 ++++---- src/volatility/utils.rs | 83 +++++++++++++++++++++----------- 13 files changed, 172 insertions(+), 99 deletions(-) diff --git a/src/error/options.rs b/src/error/options.rs index b8622f42..8092b727 100644 --- a/src/error/options.rs +++ b/src/error/options.rs @@ -233,7 +233,7 @@ pub enum OptionsError { /// * Expiration and time value calculations /// * Option payoff analysis /// -/// `OptionsError`: enum.OptionsError.html +/// See `OptionsError` for the full variant list. pub type OptionsResult = Result; /// Helper methods for creating common options errors. diff --git a/src/model/decimal.rs b/src/model/decimal.rs index bd2ba8d6..d761f8e7 100644 --- a/src/model/decimal.rs +++ b/src/model/decimal.rs @@ -233,10 +233,12 @@ pub fn f64_to_decimal(value: f64) -> Result { /// # Panics /// /// The `unreachable!` branch fires only if -/// `statrs::distribution::Normal::new(0.0, 1.0)` rejects the standard -/// normal parameters — a provably impossible state the standard -/// library upholds at all supported versions. Kept as a panic -/// rather than `Result` to preserve the infallible sampling API. +/// `statrs::distribution::Normal::new(0.0, 1.0)` rejects the provided +/// parameters. `statrs` accepts any finite mean together with a +/// strictly positive standard deviation, so `(0.0, 1.0)` is always +/// valid under the `statrs` contract and the arm is unreachable in +/// practice. Kept as a panic rather than `Result` to preserve the +/// infallible sampling API. pub fn decimal_normal_sample() -> Decimal { let mut t_rng = rand::rng(); // Normal::new(0.0, 1.0) is provably valid (mean=0, std=1 are accepted diff --git a/src/model/position.rs b/src/model/position.rs index 942e2b01..12fe41f2 100644 --- a/src/model/position.rs +++ b/src/model/position.rs @@ -244,10 +244,13 @@ impl Position { /// /// # Errors /// - /// Returns [`PositionError`] wrapping a [`PositionValidationErrorKind::InvalidPosition`] - /// when the total cost computation underflows into a negative - /// `Positive`-representable value (typically a short position where the - /// received premium exceeds the declared costs). + /// Currently infallible — long positions accumulate only + /// non-negative `Positive` terms and short positions delegate + /// to `fees()`, which is itself infallible under the current + /// implementation. The `Result` signature is retained so future + /// implementations that validate fee configuration or surface + /// overflow on `Positive` arithmetic can return + /// `PositionError` without a breaking change. pub fn total_cost(&self) -> Result { let total_cost = match self.option.side { Side::Long => (self.premium + self.open_fee + self.close_fee) * self.option.quantity, @@ -302,10 +305,13 @@ impl Position { /// /// # Errors /// - /// Returns [`PositionError::invalid_position_type`] for a long position - /// (which never receives a premium), or wraps a `Positive`-conversion - /// failure when `premium × quantity` cannot be represented as a - /// non-negative decimal. + /// Currently infallible — long positions return + /// `Ok(Positive::ZERO)` (no premium received) and short + /// positions return `Ok(self.premium * self.option.quantity)`. + /// The `Result` signature is retained so future implementations + /// that treat long-side calls as a programmer error, or that + /// surface overflow on the `premium × quantity` product, can + /// return `PositionError` without a breaking change. pub fn premium_received(&self) -> Result { match self.option.side { Side::Long => Ok(Positive::ZERO), @@ -333,10 +339,14 @@ impl Position { /// /// # Errors /// - /// Returns [`PositionError`] wrapping a - /// [`PositionValidationErrorKind::InvalidPosition`] when the net - /// amount (premium minus fees) becomes negative, signalling a - /// guaranteed-loss short position. + /// Propagates any `PositionError` raised by `total_cost()` (no + /// variant is surfaced under the current implementation, but + /// the call site keeps the `?` for forward compatibility). + /// + /// When the short-side net amount `premium − total_cost` is + /// negative the function returns `Ok(Positive::ZERO)` (clamped) + /// rather than an error, so a guaranteed-loss short position is + /// reported as zero net received rather than as a failure. pub fn net_premium_received(&self) -> Result { match self.option.side { Side::Long => Ok(Positive::ZERO), diff --git a/src/model/profit_range.rs b/src/model/profit_range.rs index 68b322be..204be1ed 100644 --- a/src/model/profit_range.rs +++ b/src/model/profit_range.rs @@ -46,10 +46,12 @@ impl ProfitLossRange { /// /// # Errors /// - /// Returns [`ProbabilityError`] wrapping a - /// `ProbabilityCalculationErrorKind::InvalidProbability` when `lower_bound` - /// is greater than `upper_bound`, or `ProbabilityCalculationErrorKind::InvalidProbabilityRange` - /// when the `probability` value lies outside the closed interval `[0, 1]`. + /// Returns `ProbabilityError::RangeError` wrapping a + /// `ProfitLossRangeErrorKind::InvalidProfitRange` when both + /// bounds are present and `lower_bound >= upper_bound`. The + /// constructor does not currently validate that `probability` + /// lies in `[0, 1]`; values outside that interval are accepted + /// and must be validated upstream. pub fn new( lower_bound: Option, upper_bound: Option, diff --git a/src/simulation/steps/x.rs b/src/simulation/steps/x.rs index c81c4733..942a9284 100644 --- a/src/simulation/steps/x.rs +++ b/src/simulation/steps/x.rs @@ -243,10 +243,18 @@ where /// /// # Errors /// - /// Returns [`SimulationError::ExpirationDate`] when the - /// underlying [`ExpirationDate::get_days`] call fails, or - /// [`SimulationError::IndexConversion`] when the step index - /// decrement would underflow `i32::MIN`. + /// Returns `SimulationError::ExpirationDate` when the underlying + /// `ExpirationDate::get_days` call fails, or + /// `SimulationError::step_error` when the step-size conversion + /// to `Positive` fails. + /// + /// # Panics + /// + /// The decrement `self.index - 1` is unchecked and will panic in + /// debug builds (or silently wrap in release) if `self.index` is + /// already `i32::MIN`. In practice simulations start at `0` and + /// only call `previous` a bounded number of times, so this is + /// treated as a caller invariant rather than a runtime check. pub fn previous(&self) -> Result { let days = self.datetime.get_days()?; let days_to_rest = convert_time_frame( diff --git a/src/simulation/traits.rs b/src/simulation/traits.rs index cf1ef365..3adf578a 100644 --- a/src/simulation/traits.rs +++ b/src/simulation/traits.rs @@ -106,10 +106,16 @@ where /// /// # Errors /// - /// Returns [`SimulationError::InvalidWalkType`] when - /// `params.walk_type` is not a [`WalkType::Brownian`] variant, - /// and [`SimulationError::PositiveError`] when a generated sample - /// cannot be represented as `Positive`. + /// Returns `SimulationError::InvalidWalkType` when + /// `params.walk_type` is not a `WalkType::Brownian` variant, and + /// propagates `SimulationError::PositiveError` from + /// `params.ystep_as_positive()?` when the initial `y` value + /// violates the `Positive` invariant. + /// + /// Generated samples are clamped with + /// `Positive::new_decimal(x.max(Decimal::ZERO)) + /// .unwrap_or(Positive::ZERO)`, so negative realisations do not + /// surface an error - they are replaced by `Positive::ZERO`. fn brownian(&self, params: &WalkParams) -> Result, SimulationError> { match params.walk_type { WalkType::Brownian { diff --git a/src/strategies/collar.rs b/src/strategies/collar.rs index 16adcc14..d54697b4 100644 --- a/src/strategies/collar.rs +++ b/src/strategies/collar.rs @@ -378,11 +378,14 @@ impl Collar { /// /// # Errors /// - /// Returns `PricingError::MethodError` with method `collar` when - /// the short-call strike is below the spot cost basis (the strategy - /// is not economically well-formed). Arithmetic follows - /// `PricingError::ArithmeticFailure` if fee or premium conversions - /// overflow. + /// Currently infallible — both branches compute + /// `Positive::new_decimal(total_profit.max(Decimal::ZERO)) + /// .unwrap_or(Positive::ZERO)`, so negative decompositions are + /// clamped to `Positive::ZERO` rather than surfaced as an error. + /// The `Result` signature is retained so future implementations + /// that add checked arithmetic or validate the + /// `call_strike ≥ cost_basis` precondition can return + /// `PricingError::MethodError` without a breaking change. pub fn max_profit_potential(&self) -> Result { let call_strike = self.call_strike(); let cost_basis = self.spot_leg.cost_basis; @@ -408,11 +411,14 @@ impl Collar { /// /// # Errors /// - /// Returns `PricingError::MethodError` with method `collar` when - /// the loss decomposition underflows (for example when the put strike - /// equals or exceeds the spot cost basis and the net premium net of - /// fees is positive), or when arithmetic fails while converting - /// `Positive` operands. + /// Currently infallible — both branches compute + /// `Positive::new_decimal(total_loss.max(Decimal::ZERO)) + /// .unwrap_or(Positive::ZERO)`, so negative decompositions are + /// clamped to `Positive::ZERO` rather than surfaced as an error. + /// The `Result` signature is retained so future implementations + /// that add checked arithmetic or validate the + /// `cost_basis ≥ put_strike` precondition can return + /// `PricingError::MethodError` without a breaking change. pub fn max_loss_potential(&self) -> Result { let put_strike = self.put_strike(); let cost_basis = self.spot_leg.cost_basis; diff --git a/src/strategies/covered_call.rs b/src/strategies/covered_call.rs index d9ba8636..a1c75ab1 100644 --- a/src/strategies/covered_call.rs +++ b/src/strategies/covered_call.rs @@ -294,10 +294,13 @@ impl CoveredCall { /// /// # Errors /// - /// Returns `PricingError::MethodError` with method `covered_call` - /// when the call strike sits below the spot cost basis (the strategy - /// has no upside), or when arithmetic on `Positive` operands - /// overflows. + /// Currently infallible — when `strike >= cost_basis` the + /// function returns the capital gain plus premium minus fees as + /// an `Ok(...)`, and otherwise clamps any negative worst-case + /// to `Ok(Positive::ZERO)`. The `Result` signature is retained + /// so future implementations that add checked arithmetic or + /// validate the strike layout can return + /// `PricingError::MethodError` without a breaking change. pub fn max_profit_potential(&self) -> Result { let strike = self.call_strike(); let cost_basis = self.spot_leg.cost_basis; @@ -327,10 +330,13 @@ impl CoveredCall { /// /// # Errors /// - /// Returns `PricingError::MethodError` with method `covered_call` - /// when the premium net of fees exceeds `cost_basis × quantity` - /// (which would imply a non-negative worst case), or when arithmetic - /// on `Positive` operands overflows. + /// Currently infallible. When `total_investment + total_fees` + /// exceeds `premium_received` the function returns the positive + /// delta, otherwise it clamps to `Ok(Positive::ZERO)`. The + /// `Result` signature is retained so future implementations + /// that add checked arithmetic or validate the premium/cost + /// relationship can return `PricingError::MethodError` without + /// a breaking change. pub fn max_loss_potential(&self) -> Result { let cost_basis = self.spot_leg.cost_basis; let quantity = self.spot_leg.quantity; diff --git a/src/strategies/protective_put.rs b/src/strategies/protective_put.rs index b014e67a..b291fb15 100644 --- a/src/strategies/protective_put.rs +++ b/src/strategies/protective_put.rs @@ -184,11 +184,13 @@ impl ProtectivePut { /// /// # Errors /// - /// Returns `PricingError::MethodError` with method - /// `protective_put` when the put strike equals or exceeds the spot - /// cost basis (the hedge fully neutralises downside and the - /// decomposition is ill-posed), or when arithmetic on `Positive` - /// operands overflows. + /// Currently infallible — both branches compute + /// `Positive::new_decimal(total_loss.max(Decimal::ZERO)) + /// .unwrap_or(Positive::ZERO)`, so any negative decomposition is + /// clamped to `Positive::ZERO` rather than surfaced as an error. + /// The `Result` signature is retained so future implementations + /// that add checked arithmetic or validate the strike layout can + /// return `PricingError::MethodError` without a breaking change. pub fn max_loss_potential(&self) -> Result { let put_strike = self.put_strike(); let cost_basis = self.spot_leg.cost_basis; diff --git a/src/surfaces/types.rs b/src/surfaces/types.rs index 58bd9bfe..85626c12 100644 --- a/src/surfaces/types.rs +++ b/src/surfaces/types.rs @@ -110,10 +110,15 @@ impl Point3D { /// /// # Errors /// - /// Returns [`SurfaceError::Point3DError`] when one of the - /// `Decimal` coordinates cannot be converted into the target - /// numeric type `T`, `U` or `V` (e.g. out-of-range for `f32`, - /// overflow for `u32`). + /// Returns `SurfaceError::Point3DError` in three situations: + /// + /// - when one of the target types is `Positive`-like + /// (detected via the `is_positive::()` / `is_positive::()` + /// / `is_positive::()` checks) and the corresponding + /// coordinate is less than or equal to zero. + /// - when one of the `Decimal` coordinates cannot be converted + /// into the target numeric type (e.g. out-of-range for `f32`, + /// overflow for `u32`). pub fn to_tuple< T: TryFrom + 'static, U: TryFrom + 'static, diff --git a/src/visualization/plotly.rs b/src/visualization/plotly.rs index ec636b8a..54d0b7c8 100644 --- a/src/visualization/plotly.rs +++ b/src/visualization/plotly.rs @@ -327,10 +327,10 @@ pub trait Graph { /// /// # Errors /// - /// Returns `GraphError::RenderError` when the chosen - /// [`OutputType`] backend (PNG/SVG via static_export, HTML, etc.) - /// fails to serialize or render, and `GraphError::IoError` when - /// the destination path cannot be written. + /// Returns `GraphError::Render` when the chosen `OutputType` + /// backend (PNG/SVG via `static_export`, HTML, etc.) fails to + /// serialize or render, and `GraphError::Io` when the + /// destination path cannot be written. #[cfg(feature = "plotly")] fn render(&self, output: OutputType) -> Result<(), GraphError> { match output { diff --git a/src/volatility/traits.rs b/src/volatility/traits.rs index 1a8141a7..37fc3071 100644 --- a/src/volatility/traits.rs +++ b/src/volatility/traits.rs @@ -87,24 +87,21 @@ pub trait VolatilitySmile { /// Implementations should return a `Positive` value representing the ATM IV, or an error /// if the value cannot be determined. pub trait AtmIvProvider { - /// Get the at-the-money implied volatility - /// - /// This method attempts to return the at-the-money implied volatility as an `Option`. + /// Get the at-the-money implied volatility. /// /// # Returns /// - /// * `Ok(Some(Positive))` - If the ATM implied volatility is successfully retrieved. - /// * `Ok(None)` - If the ATM implied volatility is not available or not applicable. - /// * `Err(Box)` - If an error occurs during the retrieval process. + /// * `Ok(&Positive)` - a reference to the ATM implied volatility + /// maintained by the implementor. + /// * `Err(VolatilityError)` - when the implementor cannot supply + /// an ATM IV for the current observation. /// /// # Errors /// - /// Returns [`VolatilityError::AtmIvUnavailable`] with a wrapped - /// [`VolatilityError::Chain`] source when the underlying container - /// (typically an option chain) cannot supply an ATM implied - /// volatility for the current observation, or - /// [`VolatilityError::IvNotFound`] when no matching strike produced - /// a valid candidate. + /// Returns `VolatilityError::AtmIvUnavailable` with a wrapped + /// `VolatilityError::Chain` source when the underlying container + /// (typically an option chain) cannot resolve an ATM implied + /// volatility for the current observation. fn atm_iv(&self) -> Result<&Positive, VolatilityError>; } diff --git a/src/volatility/utils.rs b/src/volatility/utils.rs index 2294f96d..aa25e861 100644 --- a/src/volatility/utils.rs +++ b/src/volatility/utils.rs @@ -29,10 +29,14 @@ use positive::pos_or_panic; /// /// # Errors /// -/// Returns [`VolatilityError::NumericalFailure`] when the length of +/// Returns `VolatilityError::NumericalFailure` when the length of /// `returns` cannot be represented as a `Decimal` or when the -/// variance computation overflows, and [`VolatilityError::PositiveError`] -/// if the final square-root produces a non-positive candidate. +/// final `variance.sqrt()` fails (overflow). +/// +/// When `returns.len() < 2` the function returns `Ok(Positive::ZERO)` +/// rather than an error. Conversion of the final std-dev into +/// `Positive` is clamped to `Positive::ZERO`, so `PositiveError` is +/// never surfaced. pub fn constant_volatility(returns: &[Decimal]) -> Result { let n_dec = Decimal::from_usize(returns.len()).ok_or_else(|| VolatilityError::NumericalFailure { @@ -72,10 +76,16 @@ pub fn constant_volatility(returns: &[Decimal]) -> Result returns.len()` the +/// function returns `Ok(vec![])` instead of an error, so callers +/// that need a non-empty result must validate the inputs +/// themselves. pub fn historical_volatility( returns: &[Decimal], window_size: usize, @@ -99,10 +109,14 @@ pub fn historical_volatility( /// /// # Errors /// -/// Returns [`VolatilityError::NumericalFailure`] when the EWMA -/// recurrence overflows or produces a negative running variance, and -/// [`VolatilityError::PositiveError`] when a running square-root -/// violates the `Positive` invariant. +/// Returns `VolatilityError::NumericalFailure` when `returns` is +/// empty or when any `variance.sqrt()` call fails (negative operand +/// reported as a generic overflow failure). +/// +/// Each running std-dev is converted via +/// `Positive::new_decimal(...).unwrap_or(Positive::ZERO)`, so +/// `PositiveError` is never surfaced — negative intermediates are +/// already caught by the `sqrt` check above. pub fn ewma_volatility( returns: &[Decimal], lambda: Decimal, @@ -155,12 +169,16 @@ pub fn ewma_volatility( /// /// # Errors /// -/// Returns [`VolatilityError::NoConvergence`] when the Newton–Raphson -/// iteration exhausts `MAX_ITERATIONS_IV` without matching the target -/// price, [`VolatilityError::IvNotFound`] when the search grid never -/// produced a valid candidate, or propagates -/// [`VolatilityError::Options`] from the underlying Black–Scholes -/// evaluation on each iteration. +/// The implementation is a parallel grid search over +/// `100 * max_iterations` candidate volatilities rather than a +/// Newton–Raphson iteration. It returns +/// `VolatilityError::IvNotFound` when the best candidate is the +/// lower boundary `1 / (100 * max_iterations)` (the search never +/// improved), `VolatilityError::NoValidVolatility` when every grid +/// point failed the Black–Scholes evaluation, and +/// `VolatilityError::PositiveError` when the boundary `Positive` +/// conversion fails. Black–Scholes errors on individual candidates +/// are discarded by the parallel filter rather than propagated. pub fn implied_volatility( market_price: Positive, options: &mut Options, @@ -268,11 +286,17 @@ pub fn calculate_iv( /// /// # Errors /// -/// Returns [`VolatilityError::NumericalFailure`] when the GARCH -/// recurrence overflows, and [`VolatilityError::PositiveError`] when a -/// running square-root produces a non-positive candidate (typically -/// when `omega`, `alpha` or `beta` violate the GARCH(1,1) stationarity -/// constraint). +/// Currently infallible — every `Positive::new_decimal(...)` call +/// is clamped to `Positive::ZERO`, so neither `NumericalFailure` nor +/// `PositiveError` is surfaced. The `Result` signature is retained +/// so future implementations that add explicit stationarity checks +/// (`alpha + beta < 1`) or overflow validation can return +/// `VolatilityError::NumericalFailure` without a breaking change. +/// +/// # Panics +/// +/// Panics on `returns[0]` when `returns` is empty. Callers must +/// validate the input slice length before invoking. pub fn garch_volatility( returns: &[Decimal], omega: Decimal, @@ -306,11 +330,16 @@ pub fn garch_volatility( /// /// # Errors /// -/// Returns [`VolatilityError::NumericalFailure`] when the Heston -/// simulation kernel overflows a `Decimal` step or when a square-root -/// operand turns negative under the truncated-Euler scheme, and -/// [`VolatilityError::PositiveError`] when the resulting variance -/// breaches the `Positive` invariant. +/// Returns `VolatilityError::NumericalFailure` when the initial +/// `sqrt(dt)` call overflows, when that square root is not +/// representable as `f64`, or when the per-step `dw` sample cannot +/// be converted back into `Decimal`. +/// +/// Variance is clamped to zero on each step +/// (`v = v.max(Decimal::ZERO)`) and converted via +/// `Positive::new_decimal(...).unwrap_or(Positive::ZERO)`, so +/// negative variance is silently recovered and `PositiveError` is +/// never surfaced. pub fn simulate_heston_volatility( kappa: Decimal, theta: Decimal,