diff --git a/README.md b/README.md index 9c85b587..b4aab362 100644 --- a/README.md +++ b/README.md @@ -803,7 +803,7 @@ let underlying_price = Positive::HUNDRED; pos_or_panic!(1.20), // short_call_close_fee Default::default(), Default::default(), Default::default(), Default::default() - ); + )?; // Analyze the strategy tracing::info!("Strategy: {}", strategy.get_title()); @@ -934,7 +934,7 @@ let strategy = CustomStrategy::new( Positive::ONE, 30, implied_volatility, -); +).expect("valid custom strategy"); tracing::info!("Strategy created: {}", strategy.get_title()); ``` diff --git a/examples/examples_simulation/src/bin/historical_build_chain.rs b/examples/examples_simulation/src/bin/historical_build_chain.rs index cadd97f4..f09347be 100644 --- a/examples/examples_simulation/src/bin/historical_build_chain.rs +++ b/examples/examples_simulation/src/bin/historical_build_chain.rs @@ -85,11 +85,9 @@ fn main() -> Result<(), Error> { walker, }; - let random_walk = RandomWalk::new( - "Random Walk".to_string(), - &walk_params, - generator_optionchain, - ); + let random_walk = RandomWalk::new("Random Walk".to_string(), &walk_params, |p| { + generator_optionchain(p).expect("generator_optionchain failed") + }); debug!("Random Walk: {}", random_walk); let path: &Path = "Draws/Simulation/historical_build_chain.png".as_ref(); random_walk.write_png(path)?; diff --git a/examples/examples_simulation/src/bin/long_call_strategy_simulation.rs b/examples/examples_simulation/src/bin/long_call_strategy_simulation.rs index aa691fa3..d0332a2c 100644 --- a/examples/examples_simulation/src/bin/long_call_strategy_simulation.rs +++ b/examples/examples_simulation/src/bin/long_call_strategy_simulation.rs @@ -159,7 +159,7 @@ fn main() -> Result<(), Error> { "Long Call Simulator".to_string(), n_simulations, &walk_params, - generator_positive, + |p| generator_positive(p).expect("generator_positive failed"), ); info!("Running simulations using Simulate trait..."); diff --git a/examples/examples_simulation/src/bin/position_simulator.rs b/examples/examples_simulation/src/bin/position_simulator.rs index 6cd56562..6b1775a6 100644 --- a/examples/examples_simulation/src/bin/position_simulator.rs +++ b/examples/examples_simulation/src/bin/position_simulator.rs @@ -36,12 +36,9 @@ fn main() -> Result<(), Error> { walker, }; - let simulator = Simulator::new( - "Simulator".to_string(), - simulator_size, - &walk_params, - generator_positive, - ); + let simulator = Simulator::new("Simulator".to_string(), simulator_size, &walk_params, |p| { + generator_positive(p).expect("generator_positive failed") + }); debug!("Simulator: {}", simulator); let option: Options = Options::new( diff --git a/examples/examples_simulation/src/bin/random_walk.rs b/examples/examples_simulation/src/bin/random_walk.rs index 1c613110..5e199285 100644 --- a/examples/examples_simulation/src/bin/random_walk.rs +++ b/examples/examples_simulation/src/bin/random_walk.rs @@ -34,7 +34,9 @@ fn main() -> Result<(), Error> { walker, }; - let random_walk = RandomWalk::new("Random Walk".to_string(), &walk_params, generator_positive); + let random_walk = RandomWalk::new("Random Walk".to_string(), &walk_params, |p| { + generator_positive(p).expect("generator_positive failed") + }); debug!("Random Walk: {}", random_walk); let path: &std::path::Path = "Draws/Simulation/random_walk.png".as_ref(); random_walk.write_png(path)?; diff --git a/examples/examples_simulation/src/bin/random_walk_build_chain.rs b/examples/examples_simulation/src/bin/random_walk_build_chain.rs index 0d9fa506..6763c85f 100644 --- a/examples/examples_simulation/src/bin/random_walk_build_chain.rs +++ b/examples/examples_simulation/src/bin/random_walk_build_chain.rs @@ -67,11 +67,9 @@ fn main() -> Result<(), Error> { walker, }; - let random_walk = RandomWalk::new( - "Random Walk".to_string(), - &walk_params, - generator_optionchain, - ); + let random_walk = RandomWalk::new("Random Walk".to_string(), &walk_params, |p| { + generator_optionchain(p).expect("generator_optionchain failed") + }); debug!("Random Walk: {}", random_walk); let path: &std::path::Path = "Draws/Simulation/random_walk_build_chain.png".as_ref(); random_walk.write_png(path)?; diff --git a/examples/examples_simulation/src/bin/random_walk_chain.rs b/examples/examples_simulation/src/bin/random_walk_chain.rs index 42c562dc..d54c2e88 100644 --- a/examples/examples_simulation/src/bin/random_walk_chain.rs +++ b/examples/examples_simulation/src/bin/random_walk_chain.rs @@ -37,11 +37,9 @@ fn main() -> Result<(), Error> { walker, }; - let random_walk = RandomWalk::new( - "Random Walk".to_string(), - &walk_params, - generator_optionchain, - ); + let random_walk = RandomWalk::new("Random Walk".to_string(), &walk_params, |p| { + generator_optionchain(p).expect("generator_optionchain failed") + }); debug!("Random Walk: {}", random_walk); let path: &std::path::Path = "Draws/Simulation/random_walk_chain.png".as_ref(); diff --git a/examples/examples_simulation/src/bin/short_put_simulation.rs b/examples/examples_simulation/src/bin/short_put_simulation.rs index a5193023..b98939e7 100644 --- a/examples/examples_simulation/src/bin/short_put_simulation.rs +++ b/examples/examples_simulation/src/bin/short_put_simulation.rs @@ -410,7 +410,7 @@ fn main() -> Result<(), Error> { "Short Put Simulator".to_string(), n_simulations, &walk_params, - generator_positive, + |p| generator_positive(p).expect("generator_positive failed"), ); // Create progress bar diff --git a/examples/examples_simulation/src/bin/short_put_strategy_simulation.rs b/examples/examples_simulation/src/bin/short_put_strategy_simulation.rs index 2f780a2e..c77af7be 100644 --- a/examples/examples_simulation/src/bin/short_put_strategy_simulation.rs +++ b/examples/examples_simulation/src/bin/short_put_strategy_simulation.rs @@ -152,7 +152,7 @@ fn main() -> Result<(), Error> { "Short Put Simulator".to_string(), n_simulations, &walk_params, - generator_positive, + |p| generator_positive(p).expect("generator_positive failed"), ); info!("Running simulations using Simulate trait..."); diff --git a/examples/examples_simulation/src/bin/simulator.rs b/examples/examples_simulation/src/bin/simulator.rs index 3439e3fc..0d4fe72f 100644 --- a/examples/examples_simulation/src/bin/simulator.rs +++ b/examples/examples_simulation/src/bin/simulator.rs @@ -37,12 +37,9 @@ fn main() -> Result<(), Error> { walker, }; - let simulator = Simulator::new( - "Simulator".to_string(), - simulator_size, - &walk_params, - generator_positive, - ); + let simulator = Simulator::new("Simulator".to_string(), simulator_size, &walk_params, |p| { + generator_positive(p).expect("generator_positive failed") + }); debug!("Simulator: {}", simulator); // let last_steps: Vec<&Step> = simulator diff --git a/examples/examples_simulation/src/bin/strategy_simulator.rs b/examples/examples_simulation/src/bin/strategy_simulator.rs index c20c248a..061cc14b 100644 --- a/examples/examples_simulation/src/bin/strategy_simulator.rs +++ b/examples/examples_simulation/src/bin/strategy_simulator.rs @@ -53,12 +53,9 @@ fn main() -> Result<(), Error> { walker, }; - let simulator = Simulator::new( - "Simulator".to_string(), - simulator_size, - &walk_params, - generator_positive, - ); + let simulator = Simulator::new("Simulator".to_string(), simulator_size, &walk_params, |p| { + generator_positive(p).expect("generator_positive failed") + }); debug!("Simulator: {}", simulator); info!("Open Premium: ${:.2}", open_premium); diff --git a/src/chains/chain.rs b/src/chains/chain.rs index f3a16e88..c9609512 100644 --- a/src/chains/chain.rs +++ b/src/chains/chain.rs @@ -326,6 +326,7 @@ impl OptionChain { /// # Examples /// /// ``` + /// # fn run() -> Result<(), Box> { /// use rust_decimal_macros::dec; /// use optionstratlib::chains::utils::{OptionChainBuildParams, OptionDataPriceParams}; /// use positive::{pos_or_panic, spos, Positive}; @@ -352,7 +353,9 @@ impl OptionChain { /// pos_or_panic!(0.2) // implied volatility /// ); /// - /// let chain = OptionChain::build_chain(&build_params).unwrap(); + /// let chain = OptionChain::build_chain(&build_params)?; + /// # Ok(()) + /// # } /// ``` /// Builds a complete option chain based on the provided parameters. /// @@ -563,18 +566,9 @@ impl OptionChain { let strike_prices: Vec = self.options.iter().map(|opt| opt.strike_price).collect(); - if !strike_prices.is_empty() { - // Find the maximum distance from ATM in number of strikes - // SAFETY: We just checked that strike_prices is not empty - let min_strike = strike_prices - .iter() - .min() - .expect("strike_prices is not empty"); - let max_strike = strike_prices - .iter() - .max() - .expect("strike_prices is not empty"); - + if let Some((min_strike, max_strike)) = + strike_prices.iter().min().zip(strike_prices.iter().max()) + { let strikes_below = ((atm_strike.to_dec() - min_strike.to_dec()) / strike_interval.to_dec()) .ceil() diff --git a/src/chains/generators.rs b/src/chains/generators.rs index ac6d8d1a..e13c5386 100644 --- a/src/chains/generators.rs +++ b/src/chains/generators.rs @@ -66,47 +66,51 @@ fn create_chain_from_step( /// /// # Returns /// -/// * `Vec>` - A vector of `Step`s representing the simulated walk. +/// * `Ok(Vec>)` - A vector of `Step`s representing the simulated walk. +/// * `Err(ChainError)` - If the underlying simulator, volatility helpers, or chain construction +/// fails for any reason. /// +/// # Errors +/// +/// Returns `ChainError::DynError` (via the `From` / `From` +/// conversions) if the random-walk generator returns an error, the historical helpers +/// (`calculate_log_returns`, `constant_volatility`, `adjust_volatility`) cannot complete, +/// or `create_chain_from_step` fails to rebuild the chain. pub fn generator_optionchain( walk_params: &WalkParams, -) -> Vec> { +) -> Result>, ChainError> { debug!("{}", walk_params); let (mut y_steps, volatility) = match &walk_params.walk_type { - WalkType::Brownian { volatility, .. } => ( - walk_params.walker.brownian(walk_params).unwrap(), - Some(*volatility), - ), + WalkType::Brownian { volatility, .. } => { + (walk_params.walker.brownian(walk_params)?, Some(*volatility)) + } WalkType::GeometricBrownian { volatility, .. } => ( - walk_params.walker.geometric_brownian(walk_params).unwrap(), + walk_params.walker.geometric_brownian(walk_params)?, Some(*volatility), ), WalkType::LogReturns { volatility, .. } => ( - walk_params.walker.log_returns(walk_params).unwrap(), + walk_params.walker.log_returns(walk_params)?, Some(*volatility), ), WalkType::MeanReverting { volatility, .. } => ( - walk_params.walker.mean_reverting(walk_params).unwrap(), + walk_params.walker.mean_reverting(walk_params)?, Some(*volatility), ), WalkType::JumpDiffusion { volatility, .. } => ( - walk_params.walker.jump_diffusion(walk_params).unwrap(), - Some(*volatility), - ), - WalkType::Garch { volatility, .. } => ( - walk_params.walker.garch(walk_params).unwrap(), - Some(*volatility), - ), - WalkType::Heston { volatility, .. } => ( - walk_params.walker.heston(walk_params).unwrap(), - Some(*volatility), - ), - WalkType::Custom { volatility, .. } => ( - walk_params.walker.custom(walk_params).unwrap(), + walk_params.walker.jump_diffusion(walk_params)?, Some(*volatility), ), + WalkType::Garch { volatility, .. } => { + (walk_params.walker.garch(walk_params)?, Some(*volatility)) + } + WalkType::Heston { volatility, .. } => { + (walk_params.walker.heston(walk_params)?, Some(*volatility)) + } + WalkType::Custom { volatility, .. } => { + (walk_params.walker.custom(walk_params)?, Some(*volatility)) + } WalkType::Telegraph { volatility, .. } => ( - walk_params.walker.telegraph(walk_params).unwrap(), + walk_params.walker.telegraph(walk_params)?, Some(*volatility), ), WalkType::Historical { @@ -115,23 +119,25 @@ pub fn generator_optionchain( if prices.is_empty() || prices.len() < walk_params.size { (Vec::new(), None) } else { - let log_returns: Vec = calculate_log_returns(prices) - .unwrap() + let log_returns: Vec = calculate_log_returns(prices)? .iter() .map(|p| p.to_dec()) .collect(); - let constant_volatility = constant_volatility(&log_returns).unwrap(); + let constant_volatility = constant_volatility(&log_returns)?; let implied_volatility = - adjust_volatility(constant_volatility, *timeframe, TimeFrame::Year).unwrap(); + adjust_volatility(constant_volatility, *timeframe, TimeFrame::Year)?; ( - walk_params.walker.historical(walk_params).unwrap(), + walk_params.walker.historical(walk_params)?, Some(implied_volatility), ) } } }; if y_steps.is_empty() { - return vec![]; + // Preserve the init-step invariant when the underlying walk produces + // no points (e.g., Historical with insufficient `prices`); downstream + // consumers expect at least the initial step to be present. + return Ok(vec![walk_params.init_step.clone()]); } let _ = y_steps.remove(0); // remove initial step from y_steps to avoid early return @@ -157,8 +163,7 @@ pub fn generator_optionchain( Some(Box::new(*y_step)), volatility, Some(expiration_date), - ) - .unwrap(); + )?; previous_y_step = previous_y_step.next(y_step_chain).clone(); let step = Step { x: previous_x_step, @@ -169,7 +174,7 @@ pub fn generator_optionchain( } assert!(steps.len() <= walk_params.size); - steps + Ok(steps) } /// Generates a vector of `Step`s containing `Positive` x-values and `Positive` y-values. @@ -183,25 +188,28 @@ pub fn generator_optionchain( /// /// # Returns /// -/// * `Vec>` - A vector of `Step`s representing the simulated walk. +/// * `Ok(Vec>)` - A vector of `Step`s representing the simulated walk. +/// * `Err(ChainError)` - If the underlying simulator fails for any reason. +/// +/// # Errors /// +/// Returns `ChainError::DynError` (via the `From` conversion) if the +/// random-walk generator returns an error. pub fn generator_positive( walk_params: &WalkParams, -) -> Vec> { +) -> Result>, ChainError> { debug!("{}", walk_params); let mut y_steps = match &walk_params.walk_type { - WalkType::Brownian { .. } => walk_params.walker.brownian(walk_params).unwrap(), - WalkType::GeometricBrownian { .. } => { - walk_params.walker.geometric_brownian(walk_params).unwrap() - } - WalkType::LogReturns { .. } => walk_params.walker.log_returns(walk_params).unwrap(), - WalkType::MeanReverting { .. } => walk_params.walker.mean_reverting(walk_params).unwrap(), - WalkType::JumpDiffusion { .. } => walk_params.walker.jump_diffusion(walk_params).unwrap(), - WalkType::Garch { .. } => walk_params.walker.garch(walk_params).unwrap(), - WalkType::Heston { .. } => walk_params.walker.heston(walk_params).unwrap(), - WalkType::Custom { .. } => walk_params.walker.custom(walk_params).unwrap(), - WalkType::Telegraph { .. } => walk_params.walker.telegraph(walk_params).unwrap(), - WalkType::Historical { .. } => walk_params.walker.historical(walk_params).unwrap(), + WalkType::Brownian { .. } => walk_params.walker.brownian(walk_params)?, + WalkType::GeometricBrownian { .. } => walk_params.walker.geometric_brownian(walk_params)?, + WalkType::LogReturns { .. } => walk_params.walker.log_returns(walk_params)?, + WalkType::MeanReverting { .. } => walk_params.walker.mean_reverting(walk_params)?, + WalkType::JumpDiffusion { .. } => walk_params.walker.jump_diffusion(walk_params)?, + WalkType::Garch { .. } => walk_params.walker.garch(walk_params)?, + WalkType::Heston { .. } => walk_params.walker.heston(walk_params)?, + WalkType::Custom { .. } => walk_params.walker.custom(walk_params)?, + WalkType::Telegraph { .. } => walk_params.walker.telegraph(walk_params)?, + WalkType::Historical { .. } => walk_params.walker.historical(walk_params)?, }; let _ = y_steps.remove(0); @@ -224,7 +232,7 @@ pub fn generator_positive( } assert!(steps.len() <= walk_params.size); - steps + Ok(steps) } #[cfg(test)] @@ -298,11 +306,9 @@ mod tests { walker, }; - let random_walk = RandomWalk::new( - "Random Walk".to_string(), - &walk_params, - generator_optionchain, - ); + let random_walk = RandomWalk::new("Random Walk".to_string(), &walk_params, |p| { + generator_optionchain(p).unwrap() + }); assert_eq!(random_walk.len(), n_steps); } @@ -335,8 +341,9 @@ mod tests { }, walker, }; - let random_walk = - RandomWalk::new("Random Walk".to_string(), &walk_params, generator_positive); + let random_walk = RandomWalk::new("Random Walk".to_string(), &walk_params, |p| { + generator_positive(p).unwrap() + }); assert_eq!(random_walk.len(), n_steps); } } @@ -395,7 +402,7 @@ mod generators_coverage_tests { walker, }; - let steps = generator_optionchain(&walk_params); + let steps = generator_optionchain(&walk_params).unwrap(); // We should just get the initial step back assert_eq!(steps.len(), 1); @@ -426,7 +433,7 @@ mod generators_coverage_tests { walker, }; - let steps = generator_positive(&walk_params); + let steps = generator_positive(&walk_params).unwrap(); // We should just get the initial step back assert_eq!(steps.len(), 1); @@ -463,7 +470,7 @@ mod generators_coverage_tests { walker, }; - let steps = generator_optionchain(&walk_params); + let steps = generator_optionchain(&walk_params).unwrap(); // We should just get the initial step back assert_eq!(steps.len(), 1); @@ -501,7 +508,7 @@ mod generators_coverage_tests { walker, }; - let steps = generator_optionchain(&walk_params); + let steps = generator_optionchain(&walk_params).unwrap(); // We should just get the initial step back assert_eq!(steps.len(), 1); @@ -539,7 +546,7 @@ mod generators_coverage_tests { walker, }; - let steps = generator_optionchain(&walk_params); + let steps = generator_optionchain(&walk_params).unwrap(); // We should just get the initial step back assert_eq!(steps.len(), 1); @@ -579,7 +586,7 @@ mod generators_coverage_tests { walker, }; - let steps = generator_optionchain(&walk_params); + let steps = generator_optionchain(&walk_params).unwrap(); // We should just get the initial step back assert_eq!(steps.len(), 1); @@ -618,7 +625,7 @@ mod generators_coverage_tests { walker, }; - let steps = generator_optionchain(&walk_params); + let steps = generator_optionchain(&walk_params).unwrap(); // We should just get the initial step back assert_eq!(steps.len(), 1); @@ -659,7 +666,7 @@ mod generators_coverage_tests { walker, }; - let steps = generator_optionchain(&walk_params); + let steps = generator_optionchain(&walk_params).unwrap(); // We should just get the initial step back assert_eq!(steps.len(), 1); @@ -699,7 +706,7 @@ mod generators_coverage_tests { walker, }; - let steps = generator_optionchain(&walk_params); + let steps = generator_optionchain(&walk_params).unwrap(); // We should just get the initial step back assert_eq!(steps.len(), 1); @@ -740,7 +747,7 @@ mod generators_coverage_tests { walker, }; - let steps = generator_optionchain(&walk_params); + let steps = generator_optionchain(&walk_params).unwrap(); // We should just get the initial step back assert_eq!(steps.len(), 1); diff --git a/src/chains/mod.rs b/src/chains/mod.rs index c25e2b0f..6de0d450 100644 --- a/src/chains/mod.rs +++ b/src/chains/mod.rs @@ -21,6 +21,7 @@ //! ## Example Usage //! //! ```rust +//! # fn run() -> Result<(), Box> { //! use rust_decimal::Decimal; //! use rust_decimal_macros::dec; //! use optionstratlib::chains::OptionChain; @@ -47,9 +48,11 @@ //! pos_or_panic!(0.2), //! ); //! -//! let built_chain = OptionChain::build_chain(&option_chain_params).unwrap(); +//! let built_chain = OptionChain::build_chain(&option_chain_params)?; //! assert_eq!(built_chain.symbol, "SP500"); -//! assert_eq!(built_chain.underlying_price, Positive::new(100.0).unwrap()); +//! assert_eq!(built_chain.underlying_price, Positive::new(100.0)?); +//! # Ok(()) +//! # } //! ``` //! //! ## Strategy Legs Support @@ -113,6 +116,7 @@ //! ## Usage Example //! //! ```rust +//! # fn run() -> Result<(), Box> { //! use rust_decimal_macros::dec; //! use tracing::info; //! use optionstratlib::chains::{RNDParameters, RNDAnalysis}; @@ -128,7 +132,7 @@ //! derivative_tolerance: pos_or_panic!(0.001), //! }; //! let chain = OptionDataPriceParams::new( -//! Some(Box::new(Positive::new(2000.0).unwrap())), +//! Some(Box::new(Positive::new(2000.0)?)), //! Some(ExpirationDate::Days(pos_or_panic!(10.0))), //! Some(dec!(0.01)), //! Some(Positive::ZERO), @@ -142,21 +146,23 @@ //! Some(Positive::ONE), //! dec!(-0.2), //! dec!(0.0001), -//! Positive::new(0.02).unwrap(), +//! Positive::new(0.02)?, //! 2, //! chain, //! pos_or_panic!(0.2), //! ); //! -//! let option_chain = OptionChain::build_chain(&option_chain_params).unwrap(); +//! let option_chain = OptionChain::build_chain(&option_chain_params)?; //! // Calculate RND from option chain -//! let rnd_result = option_chain.calculate_rnd(¶ms).unwrap(); +//! let rnd_result = option_chain.calculate_rnd(¶ms)?; //! //! // Access statistical moments //! info!("Expected price: {}", rnd_result.statistics.mean); //! info!("Implied volatility: {}", rnd_result.statistics.volatility); //! info!("Market bias: {}", rnd_result.statistics.skewness); //! info!("Tail risk: {}", rnd_result.statistics.kurtosis); +//! # Ok(()) +//! # } //! ``` //! //! ## Market Insights from RND diff --git a/src/chains/optiondata.rs b/src/chains/optiondata.rs index f23e650e..0d4b46be 100644 --- a/src/chains/optiondata.rs +++ b/src/chains/optiondata.rs @@ -754,7 +754,9 @@ impl OptionData { (Ok(price), true) => { if price.is_sign_positive() { self.call_middle = Positive::new_decimal(price).ok(); - self.apply_spread(spread.unwrap(), 2); + if let Some(s) = spread { + self.apply_spread(s, 2); + } } } (Ok(price), false) => { @@ -775,7 +777,9 @@ impl OptionData { (Ok(price), true) => { if price.is_sign_positive() { self.put_middle = Positive::new_decimal(price).ok(); - self.apply_spread(spread.unwrap(), 2); + if let Some(s) = spread { + self.apply_spread(s, 2); + } } } (Ok(price), false) => { @@ -821,7 +825,7 @@ impl OptionData { match (self.call_ask, self.call_bid, self.call_middle) { (_, _, Some(call_middle)) => { - if self.call_middle.unwrap() > spread { + if call_middle > spread { self.call_ask = Some((call_middle + half_spread).round_to(decimal_places)); self.call_bid = Some( call_middle @@ -838,13 +842,15 @@ impl OptionData { } } (Some(call_ask), Some(call_bid), None) => { - trace!("apply_spread: Call middle price is None, cannot apply spread"); - self.call_ask = Some((call_ask + half_spread).round_to(decimal_places)); - self.call_bid = Some(call_bid.sub_or_zero(&half_spread).round_to(decimal_places)); - self.call_middle = Some( - ((self.call_ask.unwrap() + self.call_bid.unwrap()) / Positive::TWO) - .round_to(decimal_places), + trace!( + "apply_spread: Call middle price is None; recomputing from bid/ask after applying spread" ); + let new_ask = (call_ask + half_spread).round_to(decimal_places); + let new_bid = call_bid.sub_or_zero(&half_spread).round_to(decimal_places); + self.call_ask = Some(new_ask); + self.call_bid = Some(new_bid); + self.call_middle = + Some(((new_ask + new_bid) / Positive::TWO).round_to(decimal_places)); } _ => { trace!("apply_spread: Missing call ask or bid prices, cannot apply spread"); @@ -855,7 +861,7 @@ impl OptionData { match (self.put_ask, self.put_bid, self.put_middle) { (_, _, Some(put_middle)) => { - if self.put_middle.unwrap() > spread { + if put_middle > spread { self.put_ask = Some((put_middle + half_spread).round_to(decimal_places)); self.put_bid = Some( put_middle @@ -872,13 +878,15 @@ impl OptionData { } } (Some(put_ask), Some(put_bid), None) => { - trace!("apply_spread: Put middle price is None, cannot apply spread"); - self.put_ask = Some((put_ask + half_spread).round_to(decimal_places)); - self.put_bid = Some(put_bid.sub_or_zero(&half_spread).round_to(decimal_places)); - self.put_middle = Some( - ((self.put_ask.unwrap() + self.put_bid.unwrap()) / Positive::TWO) - .round_to(decimal_places), + trace!( + "apply_spread: Put middle price is None; recomputing from bid/ask after applying spread" ); + let new_ask = (put_ask + half_spread).round_to(decimal_places); + let new_bid = put_bid.sub_or_zero(&half_spread).round_to(decimal_places); + self.put_ask = Some(new_ask); + self.put_bid = Some(new_bid); + self.put_middle = + Some(((new_ask + new_bid) / Positive::TWO).round_to(decimal_places)); } _ => { trace!("apply_spread: Missing put ask or bid prices, cannot apply spread"); @@ -1008,12 +1016,8 @@ impl OptionData { panic!("Center should be managed by the strategy"); } FindOptimalSide::DeltaRange(min, max) => { - (self.delta_put.is_some() - && self.delta_put.unwrap() >= *min - && self.delta_put.unwrap() <= *max) - || (self.delta_call.is_some() - && self.delta_call.unwrap() >= *min - && self.delta_call.unwrap() <= *max) + self.delta_put.is_some_and(|d| d >= *min && d <= *max) + || self.delta_call.is_some_and(|d| d >= *min && d <= *max) } } } diff --git a/src/chains/rnd.rs b/src/chains/rnd.rs index dad2866d..8dba0bde 100644 --- a/src/chains/rnd.rs +++ b/src/chains/rnd.rs @@ -32,6 +32,7 @@ //! ## Usage Example //! //! ```rust +//! # fn run() -> Result<(), Box> { //! use rust_decimal::Decimal; //! use rust_decimal_macros::dec; //! use tracing::info; @@ -66,15 +67,17 @@ //! pos_or_panic!(0.1), //! ); //! -//! let option_chain = OptionChain::build_chain(&option_chain_params).unwrap(); +//! let option_chain = OptionChain::build_chain(&option_chain_params)?; //! // Calculate RND from option chain -//! let rnd_result = option_chain.calculate_rnd(¶ms).unwrap(); +//! let rnd_result = option_chain.calculate_rnd(¶ms)?; //! //! // Access statistical moments //! info!("Expected price: {}", rnd_result.statistics.mean); //! info!("Implied volatility: {}", rnd_result.statistics.variance.sqrt()); //! info!("Market bias: {}", rnd_result.statistics.skewness); //! info!("Tail risk: {}", rnd_result.statistics.kurtosis); +//! # Ok(()) +//! # } //! ``` //! //! ## Market Insights from RND @@ -351,7 +354,11 @@ impl RNDStatistics { // Convert variance to decimal and calculate std_dev let variance_dec = variance.to_dec(); - let std_dev = variance_dec.sqrt().unwrap(); + // SAFETY: variance is `Positive` and the early-return above has + // already excluded zero, so `Decimal::sqrt` cannot return None for + // a strictly positive value. Default to zero defensively to avoid + // a panic if a future invariant change ever produces NaN-like input. + let std_dev = variance_dec.sqrt().unwrap_or(Decimal::ZERO); let std_dev_4 = std_dev.powi(4); let mut fourth_moment = Decimal::ZERO; diff --git a/src/chains/utils.rs b/src/chains/utils.rs index 106b11e3..fef733b3 100644 --- a/src/chains/utils.rs +++ b/src/chains/utils.rs @@ -375,7 +375,10 @@ impl Display for OptionDataPriceParams { .map_or_else(|| "None".to_string(), |p| p.value().to_string()), self.expiration_date.map_or_else( || "None".to_string(), - |d| d.get_years().unwrap().to_string() + // SAFETY: Display impl cannot return ChainError; fall back to "n/a" if get_years fails. + |d| d + .get_years() + .map_or_else(|_| "n/a".to_string(), |y| y.to_string()) ), self.risk_free_rate .map_or_else(|| "None".to_string(), |r| (r * dec!(100.0)).to_string()), @@ -570,15 +573,31 @@ pub fn adjust_volatility( strike: &Positive, underlying_price: &Positive, // underlying_price ) -> Option { - if base_vol.is_none() { - return None; - } + let base_vol = (*base_vol)?; if strike.is_zero() { return None; } - let base_vol = base_vol.unwrap(); - let skew_slope = skew_slope.unwrap_or(SKEW_SLOPE).to_f64().unwrap(); - let smile_curve = smile_curve.unwrap_or(SKEW_SMILE_CURVE).to_f64().unwrap(); + // SAFETY: SKEW_SLOPE and SKEW_SMILE_CURVE are tiny `Decimal` constants + // (dec!(-0.2) and dec!(0.1)); `Decimal::to_f64` may only return `None` + // for values outside the f64 range. Fall back to 0.0 with a warning so + // that an unexpected non-finite override degrades gracefully instead + // of panicking. + let skew_slope = skew_slope + .unwrap_or(SKEW_SLOPE) + .to_f64() + .unwrap_or_else(|| { + tracing::warn!("adjust_volatility: skew_slope to_f64 returned None; defaulting to 0.0"); + 0.0 + }); + let smile_curve = smile_curve + .unwrap_or(SKEW_SMILE_CURVE) + .to_f64() + .unwrap_or_else(|| { + tracing::warn!( + "adjust_volatility: smile_curve to_f64 returned None; defaulting to 0.0" + ); + 0.0 + }); let m = (strike / underlying_price.to_f64()).ln(); let factor: f64 = 1.0 + skew_slope * m + smile_curve * m * m; let clamped = factor.clamp(0.01, 3.0); @@ -717,9 +736,11 @@ pub fn strike_step( bins.iter() .copied() .min_by(|a, b| { + // SAFETY: total order on Decimal; partial_cmp only returns None + // for NaN, which Decimal cannot represent. Fall back to Equal. ((a.to_dec() - raw_step.to_dec()).abs()) .partial_cmp(&(b.to_dec() - raw_step.to_dec()).abs()) - .unwrap() + .unwrap_or(std::cmp::Ordering::Equal) }) .unwrap_or(raw_step) } diff --git a/src/error/chains.rs b/src/error/chains.rs index 4f05d9e5..8252ed70 100644 --- a/src/error/chains.rs +++ b/src/error/chains.rs @@ -634,6 +634,22 @@ impl From for ChainError { } } +impl From for ChainError { + fn from(err: crate::error::SimulationError) -> Self { + ChainError::DynError { + message: format!("simulation error: {err}"), + } + } +} + +impl From for ChainError { + fn from(err: crate::error::VolatilityError) -> Self { + ChainError::DynError { + message: format!("volatility error: {err}"), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/pricing/monte_carlo.rs b/src/pricing/monte_carlo.rs index cefba7a1..e811aa4e 100644 --- a/src/pricing/monte_carlo.rs +++ b/src/pricing/monte_carlo.rs @@ -311,12 +311,9 @@ mod tests_price_option_monte_carlo { walker, }; - let simulator = Simulator::new( - "Test Simulator".to_string(), - 100, - &walk_params, - generator_positive, - ); + let simulator = Simulator::new("Test Simulator".to_string(), 100, &walk_params, |p| { + generator_positive(p).unwrap() + }); #[cfg(feature = "static_export")] simulator diff --git a/src/simulation/simulator.rs b/src/simulation/simulator.rs index ef732f90..7a3a6b26 100644 --- a/src/simulation/simulator.rs +++ b/src/simulation/simulator.rs @@ -787,12 +787,10 @@ mod tests { assert_eq!(walk_params.init_step.get_value(), &Positive::HUNDRED); assert_eq!(walk_params.y(), &Positive::HUNDRED); - let simulator = Simulator::new( - "Simulator".to_string(), - simulator_size, - &walk_params, - generator_positive, - ); + let simulator = + Simulator::new("Simulator".to_string(), simulator_size, &walk_params, |p| { + generator_positive(p).unwrap() + }); debug!("Simulator: {}", simulator); assert_eq!(simulator.get_title(), "Simulator"); assert_eq!(simulator.len(), simulator_size); diff --git a/src/strategies/long_call.rs b/src/strategies/long_call.rs index cacaa327..10eb3596 100644 --- a/src/strategies/long_call.rs +++ b/src/strategies/long_call.rs @@ -816,7 +816,9 @@ mod tests_simulate { pos_or_panic!(115.0), ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test".to_string(), 1, &walk_params, generator_positive); + let simulator = Simulator::new("Test".to_string(), 1, &walk_params, |p| { + generator_positive(p).unwrap() + }); let results = strategy.simulate(&simulator, ExitPolicy::ProfitPercent(dec!(0.5))); assert!(results.is_ok()); let stats = results.unwrap(); @@ -832,7 +834,9 @@ mod tests_simulate { pos_or_panic!(102.0), ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test".to_string(), 1, &walk_params, generator_positive); + let simulator = Simulator::new("Test".to_string(), 1, &walk_params, |p| { + generator_positive(p).unwrap() + }); let results = strategy.simulate(&simulator, ExitPolicy::Expiration); assert!(results.is_ok(), "Simulate failed: {:?}", results.err()); let stats = results.unwrap(); @@ -848,7 +852,9 @@ mod tests_simulate { pos_or_panic!(110.0), ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test".to_string(), 3, &walk_params, generator_positive); + let simulator = Simulator::new("Test".to_string(), 3, &walk_params, |p| { + generator_positive(p).unwrap() + }); let results = strategy.simulate(&simulator, ExitPolicy::Expiration); assert!(results.is_ok(), "Simulate failed: {:?}", results.err()); let stats = results.unwrap(); diff --git a/src/strategies/long_put.rs b/src/strategies/long_put.rs index 726e8a7c..7177d1d6 100644 --- a/src/strategies/long_put.rs +++ b/src/strategies/long_put.rs @@ -825,7 +825,9 @@ mod tests_simulate { pos_or_panic!(85.0), ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test".to_string(), 1, &walk_params, generator_positive); + let simulator = Simulator::new("Test".to_string(), 1, &walk_params, |p| { + generator_positive(p).unwrap() + }); let results = strategy.simulate(&simulator, ExitPolicy::ProfitPercent(dec!(0.5))); assert!(results.is_ok()); let stats = results.unwrap(); @@ -837,7 +839,9 @@ mod tests_simulate { let strategy = create_test_long_put(); let prices = vec![Positive::HUNDRED, pos_or_panic!(99.0), pos_or_panic!(98.0)]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test".to_string(), 1, &walk_params, generator_positive); + let simulator = Simulator::new("Test".to_string(), 1, &walk_params, |p| { + generator_positive(p).unwrap() + }); let results = strategy.simulate(&simulator, ExitPolicy::Expiration); assert!(results.is_ok(), "Simulate failed: {:?}", results.err()); let stats = results.unwrap(); @@ -849,7 +853,9 @@ mod tests_simulate { let strategy = create_test_long_put(); let prices = vec![Positive::HUNDRED, pos_or_panic!(95.0), pos_or_panic!(90.0)]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test".to_string(), 3, &walk_params, generator_positive); + let simulator = Simulator::new("Test".to_string(), 3, &walk_params, |p| { + generator_positive(p).unwrap() + }); let results = strategy.simulate(&simulator, ExitPolicy::Expiration); assert!(results.is_ok(), "Simulate failed: {:?}", results.err()); let stats = results.unwrap(); diff --git a/src/strategies/short_call.rs b/src/strategies/short_call.rs index 97dc2f9e..db1ed4da 100644 --- a/src/strategies/short_call.rs +++ b/src/strategies/short_call.rs @@ -834,7 +834,9 @@ mod tests_simulate { pos_or_panic!(85.0), ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test".to_string(), 1, &walk_params, generator_positive); + let simulator = Simulator::new("Test".to_string(), 1, &walk_params, |p| { + generator_positive(p).unwrap() + }); let results = strategy.simulate(&simulator, ExitPolicy::ProfitPercent(dec!(0.5))); assert!(results.is_ok()); let stats = results.unwrap(); @@ -846,7 +848,9 @@ mod tests_simulate { let strategy = create_test_short_call(); let prices = vec![Positive::HUNDRED, pos_or_panic!(99.0), pos_or_panic!(98.0)]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test".to_string(), 1, &walk_params, generator_positive); + let simulator = Simulator::new("Test".to_string(), 1, &walk_params, |p| { + generator_positive(p).unwrap() + }); let results = strategy.simulate(&simulator, ExitPolicy::Expiration); assert!(results.is_ok(), "Simulate failed: {:?}", results.err()); let stats = results.unwrap(); @@ -858,7 +862,9 @@ mod tests_simulate { let strategy = create_test_short_call(); let prices = vec![Positive::HUNDRED, pos_or_panic!(95.0), pos_or_panic!(90.0)]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test".to_string(), 3, &walk_params, generator_positive); + let simulator = Simulator::new("Test".to_string(), 3, &walk_params, |p| { + generator_positive(p).unwrap() + }); let results = strategy.simulate(&simulator, ExitPolicy::Expiration); assert!(results.is_ok(), "Simulate failed: {:?}", results.err()); let stats = results.unwrap(); diff --git a/src/strategies/short_put.rs b/src/strategies/short_put.rs index a5c163f6..297f24c5 100644 --- a/src/strategies/short_put.rs +++ b/src/strategies/short_put.rs @@ -837,12 +837,9 @@ mod tests_simulate { ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new( - "Test Simulator".to_string(), - 1, - &walk_params, - generator_positive, - ); + let simulator = Simulator::new("Test Simulator".to_string(), 1, &walk_params, |p| { + generator_positive(p).unwrap() + }); let exit_policy = ExitPolicy::ProfitPercent(dec!(0.5)); let results = strategy.simulate(&simulator, exit_policy); @@ -864,12 +861,9 @@ mod tests_simulate { ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new( - "Test Simulator".to_string(), - 1, - &walk_params, - generator_positive, - ); + let simulator = Simulator::new("Test Simulator".to_string(), 1, &walk_params, |p| { + generator_positive(p).unwrap() + }); let exit_policy = ExitPolicy::LossPercent(dec!(1.0)); let results = strategy.simulate(&simulator, exit_policy); @@ -891,12 +885,9 @@ mod tests_simulate { ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new( - "Test Simulator".to_string(), - 1, - &walk_params, - generator_positive, - ); + let simulator = Simulator::new("Test Simulator".to_string(), 1, &walk_params, |p| { + generator_positive(p).unwrap() + }); let exit_policy = ExitPolicy::Expiration; let results = strategy.simulate(&simulator, exit_policy); @@ -918,12 +909,9 @@ mod tests_simulate { ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new( - "Test Simulator".to_string(), - 1, - &walk_params, - generator_positive, - ); + let simulator = Simulator::new("Test Simulator".to_string(), 1, &walk_params, |p| { + generator_positive(p).unwrap() + }); let exit_policy = ExitPolicy::Or(vec![ ExitPolicy::ProfitPercent(dec!(0.5)), @@ -947,12 +935,9 @@ mod tests_simulate { ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new( - "Test Simulator".to_string(), - 5, - &walk_params, - generator_positive, - ); + let simulator = Simulator::new("Test Simulator".to_string(), 5, &walk_params, |p| { + generator_positive(p).unwrap() + }); let exit_policy = ExitPolicy::Expiration; let results = strategy.simulate(&simulator, exit_policy); @@ -978,12 +963,9 @@ mod tests_simulate { ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new( - "Test Simulator".to_string(), - 1, - &walk_params, - generator_positive, - ); + let simulator = Simulator::new("Test Simulator".to_string(), 1, &walk_params, |p| { + generator_positive(p).unwrap() + }); let exit_policy = ExitPolicy::TimeSteps(2); let results = strategy.simulate(&simulator, exit_policy); @@ -1000,12 +982,9 @@ mod tests_simulate { let prices = vec![Positive::HUNDRED, pos_or_panic!(95.0), pos_or_panic!(90.0)]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new( - "Test Simulator".to_string(), - 1, - &walk_params, - generator_positive, - ); + let simulator = Simulator::new("Test Simulator".to_string(), 1, &walk_params, |p| { + generator_positive(p).unwrap() + }); let exit_policy = ExitPolicy::UnderlyingBelow(pos_or_panic!(92.0)); let results = strategy.simulate(&simulator, exit_policy); @@ -1026,12 +1005,9 @@ mod tests_simulate { ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new( - "Test Simulator".to_string(), - 1, - &walk_params, - generator_positive, - ); + let simulator = Simulator::new("Test Simulator".to_string(), 1, &walk_params, |p| { + generator_positive(p).unwrap() + }); let exit_policy = ExitPolicy::And(vec![ ExitPolicy::TimeSteps(2), @@ -1054,12 +1030,9 @@ mod tests_simulate { ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new( - "Test Simulator".to_string(), - 10, - &walk_params, - generator_positive, - ); + let simulator = Simulator::new("Test Simulator".to_string(), 10, &walk_params, |p| { + generator_positive(p).unwrap() + }); let exit_policy = ExitPolicy::Expiration; let results = strategy.simulate(&simulator, exit_policy); @@ -1081,12 +1054,9 @@ mod tests_simulate { ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new( - "Test Simulator".to_string(), - 3, - &walk_params, - generator_positive, - ); + let simulator = Simulator::new("Test Simulator".to_string(), 3, &walk_params, |p| { + generator_positive(p).unwrap() + }); let exit_policy = ExitPolicy::Expiration; let results = strategy.simulate(&simulator, exit_policy); @@ -1113,12 +1083,9 @@ mod tests_simulate { ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new( - "Test Simulator".to_string(), - 1, - &walk_params, - generator_positive, - ); + let simulator = Simulator::new("Test Simulator".to_string(), 1, &walk_params, |p| { + generator_positive(p).unwrap() + }); let exit_policy = ExitPolicy::Expiration; let results = strategy.simulate(&simulator, exit_policy); @@ -1145,12 +1112,9 @@ mod tests_simulate { ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new( - "Test Simulator".to_string(), - 1, - &walk_params, - generator_positive, - ); + let simulator = Simulator::new("Test Simulator".to_string(), 1, &walk_params, |p| { + generator_positive(p).unwrap() + }); let exit_policy = ExitPolicy::ProfitPercent(dec!(0.5)); let results = strategy.simulate(&simulator, exit_policy); @@ -1170,12 +1134,9 @@ mod tests_simulate { let prices = vec![Positive::HUNDRED, pos_or_panic!(85.0), pos_or_panic!(70.0)]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new( - "Test Simulator".to_string(), - 1, - &walk_params, - generator_positive, - ); + let simulator = Simulator::new("Test Simulator".to_string(), 1, &walk_params, |p| { + generator_positive(p).unwrap() + }); let exit_policy = ExitPolicy::LossPercent(dec!(1.0)); let results = strategy.simulate(&simulator, exit_policy); @@ -1199,12 +1160,9 @@ mod tests_simulate { ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new( - "Test Simulator".to_string(), - 1, - &walk_params, - generator_positive, - ); + let simulator = Simulator::new("Test Simulator".to_string(), 1, &walk_params, |p| { + generator_positive(p).unwrap() + }); let exit_policy = ExitPolicy::Expiration; let results = strategy.simulate(&simulator, exit_policy); @@ -1230,12 +1188,9 @@ mod tests_simulate { let price_count = prices.len(); let walk_params = create_walk_params(prices); - let simulator = Simulator::new( - "Test Simulator".to_string(), - 1, - &walk_params, - generator_positive, - ); + let simulator = Simulator::new("Test Simulator".to_string(), 1, &walk_params, |p| { + generator_positive(p).unwrap() + }); let exit_policy = ExitPolicy::TimeSteps(1); let results = strategy.simulate(&simulator, exit_policy); @@ -1254,12 +1209,9 @@ mod tests_simulate { let prices = vec![Positive::HUNDRED, pos_or_panic!(102.0), pos_or_panic!(98.0)]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new( - "Test Simulator".to_string(), - 5, - &walk_params, - generator_positive, - ); + let simulator = Simulator::new("Test Simulator".to_string(), 5, &walk_params, |p| { + generator_positive(p).unwrap() + }); let exit_policy = ExitPolicy::Or(vec![ ExitPolicy::ProfitPercent(dec!(0.5)),