diff --git a/Cargo.toml b/Cargo.toml index c385755c..c7205c57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "optionstratlib" -version = "0.15.3" +version = "0.16.0" edition = "2024" authors = ["Joaquin Bejar "] description = "OptionStratLib is a comprehensive Rust library for options trading and strategy development across multiple asset classes." diff --git a/README.md b/README.md index cda30851..9c85b587 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ [![Wiki](https://img.shields.io/badge/wiki-latest-blue.svg)](https://deepwiki.com/joaquinbejar/OptionStratLib) -## OptionStratLib v0.15.3: Financial Options Library +## OptionStratLib v0.16.0: Financial Options Library ### Table of Contents 1. [Introduction](#introduction) @@ -677,7 +677,7 @@ Add OptionStratLib to your `Cargo.toml`: ```toml [dependencies] -optionstratlib = "0.15.3" +optionstratlib = "0.16.0" ``` Or use cargo to add it to your project: @@ -692,7 +692,7 @@ The library includes optional features for enhanced functionality: ```toml [dependencies] -optionstratlib = { version = "0.15.3", features = ["plotly"] } +optionstratlib = { version = "0.16.0", features = ["plotly"] } ``` - `plotly`: Enables interactive visualization using plotly.rs @@ -1042,7 +1042,7 @@ cargo test --all-features --- -**OptionStratLib v0.15.3** - Built with ❤️ in Rust for the financial community +**OptionStratLib v0.16.0** - Built with ❤️ in Rust for the financial community diff --git a/benches/model/strategy.rs b/benches/model/strategy.rs index 4e29aeca..bf09c960 100644 --- a/benches/model/strategy.rs +++ b/benches/model/strategy.rs @@ -29,6 +29,7 @@ fn create_long_call() -> LongCall { pos_or_panic!(0.5), // open_fee_long_call pos_or_panic!(0.5), // close_fee_long_call ) + .unwrap() } fn create_bull_call_spread() -> BullCallSpread { @@ -49,6 +50,7 @@ fn create_bull_call_spread() -> BullCallSpread { pos_or_panic!(0.5), // open_fee_short_call pos_or_panic!(0.5), // close_fee_short_call ) + .unwrap() } fn create_iron_condor() -> IronCondor { @@ -71,6 +73,7 @@ fn create_iron_condor() -> IronCondor { pos_or_panic!(0.5), // open_fee pos_or_panic!(0.5), // close_fee ) + .unwrap() } fn create_iron_butterfly() -> IronButterfly { @@ -92,6 +95,7 @@ fn create_iron_butterfly() -> IronButterfly { pos_or_panic!(0.5), // open_fee pos_or_panic!(0.5), // close_fee ) + .unwrap() } pub(crate) fn benchmark_strategies(c: &mut Criterion) { diff --git a/examples/examples_chain/src/bin/creator.rs b/examples/examples_chain/src/bin/creator.rs index a87ff58e..2e7a9224 100644 --- a/examples/examples_chain/src/bin/creator.rs +++ b/examples/examples_chain/src/bin/creator.rs @@ -45,7 +45,7 @@ fn main() -> Result<(), optionstratlib::error::Error> { pos_or_panic!(0.1), // close_fee_short_call pos_or_panic!(0.1), // open_fee_short_put pos_or_panic!(0.1), // close_fee_short_put - ); + )?; let max_delta = dec!(0.3); let min_delta = dec!(0.15); strategy.get_best_area( diff --git a/examples/examples_chain/src/bin/option_chain_ger40.rs b/examples/examples_chain/src/bin/option_chain_ger40.rs index b8dc42a9..d65748b3 100644 --- a/examples/examples_chain/src/bin/option_chain_ger40.rs +++ b/examples/examples_chain/src/bin/option_chain_ger40.rs @@ -36,7 +36,7 @@ fn main() -> Result<(), optionstratlib::error::Error> { pos_or_panic!(0.10), // close_fee_short_call pos_or_panic!(0.10), // open_fee_short_put pos_or_panic!(0.10), // close_fee_short_put - ); + )?; strategy.get_best_ratio( &option_chain, FindOptimalSide::DeltaRange(dec!(-0.3), dec!(0.3)), diff --git a/examples/examples_chain/src/bin/option_chain_raw.rs b/examples/examples_chain/src/bin/option_chain_raw.rs index 813bdf58..32d8a5c6 100644 --- a/examples/examples_chain/src/bin/option_chain_raw.rs +++ b/examples/examples_chain/src/bin/option_chain_raw.rs @@ -36,7 +36,7 @@ fn main() -> Result<(), optionstratlib::error::Error> { pos_or_panic!(2.2), // close_fee_short_call pos_or_panic!(1.7), // open_fee_short_put pos_or_panic!(1.7), // close_fee_short_put - ); + )?; // strategy.best_area(&option_chain, FindOptimalSide::Range(pos_or_panic!(21600.0), pos_or_panic!(21700.0) )); strategy.get_best_area(&option_chain, FindOptimalSide::Upper); diff --git a/examples/examples_chain/src/bin/option_chain_raw_delta.rs b/examples/examples_chain/src/bin/option_chain_raw_delta.rs index 5fe41f69..eb4b74e8 100644 --- a/examples/examples_chain/src/bin/option_chain_raw_delta.rs +++ b/examples/examples_chain/src/bin/option_chain_raw_delta.rs @@ -34,7 +34,7 @@ fn main() -> Result<(), optionstratlib::error::Error> { pos_or_panic!(2.2), // close_fee_short_call pos_or_panic!(1.7), // open_fee_short_put pos_or_panic!(1.7), // close_fee_short_put - ); + )?; strategy.get_best_area( &option_chain, 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 2738e19a..aa691fa3 100644 --- a/examples/examples_simulation/src/bin/long_call_strategy_simulation.rs +++ b/examples/examples_simulation/src/bin/long_call_strategy_simulation.rs @@ -105,7 +105,7 @@ fn main() -> Result<(), Error> { premium_positive, // premium paid Positive::ZERO, // open_fee Positive::ZERO, // close_fee - ); + )?; // Define exit policy: 100% profit OR expiration let exit_policy = ExitPolicy::Or(vec![ 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 6d3840f4..2f780a2e 100644 --- a/examples/examples_simulation/src/bin/short_put_strategy_simulation.rs +++ b/examples/examples_simulation/src/bin/short_put_strategy_simulation.rs @@ -102,7 +102,7 @@ fn main() -> Result<(), Error> { premium_positive, // premium received pos_or_panic!(1.5), // open_fee pos_or_panic!(1.5), // close_fee - ); + )?; // Define exit policy: 50% profit OR 100% loss let exit_policy = ExitPolicy::profit_or_loss(dec!(0.7), dec!(0.6)); diff --git a/examples/examples_simulation/src/bin/strategy_simulator.rs b/examples/examples_simulation/src/bin/strategy_simulator.rs index 5933a800..c20c248a 100644 --- a/examples/examples_simulation/src/bin/strategy_simulator.rs +++ b/examples/examples_simulation/src/bin/strategy_simulator.rs @@ -37,7 +37,7 @@ fn main() -> Result<(), Error> { open_premium, Positive::ZERO, Positive::ZERO, - ); + )?; let walk_params = WalkParams { size: n_steps, diff --git a/examples/examples_strategies/src/bin/strategy_bear_call_spread.rs b/examples/examples_strategies/src/bin/strategy_bear_call_spread.rs index 4721b38e..2aac1759 100644 --- a/examples/examples_strategies/src/bin/strategy_bear_call_spread.rs +++ b/examples/examples_strategies/src/bin/strategy_bear_call_spread.rs @@ -26,7 +26,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.78), // open_fee_long pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short - ); + )?; info!("Title: {}", strategy.get_title()); info!("Break Even Points: {:?}", strategy.break_even_points); info!( diff --git a/examples/examples_strategies/src/bin/strategy_bear_put_spread.rs b/examples/examples_strategies/src/bin/strategy_bear_put_spread.rs index 468666b8..a2563316 100644 --- a/examples/examples_strategies/src/bin/strategy_bear_put_spread.rs +++ b/examples/examples_strategies/src/bin/strategy_bear_put_spread.rs @@ -25,7 +25,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.78), // open_fee_long pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short - ); + )?; info!("Title: {}", strategy.get_title()); info!("Break Even Points: {:?}", strategy.break_even_points); info!( diff --git a/examples/examples_strategies/src/bin/strategy_bull_call_spread.rs b/examples/examples_strategies/src/bin/strategy_bull_call_spread.rs index 4bbe5df1..82cde542 100644 --- a/examples/examples_strategies/src/bin/strategy_bull_call_spread.rs +++ b/examples/examples_strategies/src/bin/strategy_bull_call_spread.rs @@ -26,7 +26,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.78), // open_fee_long pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short - ); + )?; info!("Title: {}", strategy.get_title()); info!("Break Even Points: {:?}", strategy.break_even_points); info!( diff --git a/examples/examples_strategies/src/bin/strategy_bull_put_spread.rs b/examples/examples_strategies/src/bin/strategy_bull_put_spread.rs index 906b8ad0..33068447 100644 --- a/examples/examples_strategies/src/bin/strategy_bull_put_spread.rs +++ b/examples/examples_strategies/src/bin/strategy_bull_put_spread.rs @@ -26,7 +26,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.78), // open_fee_long pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short - ); + )?; info!("Title: {}", strategy.get_title()); info!("Break Even Points: {:?}", strategy.break_even_points); diff --git a/examples/examples_strategies/src/bin/strategy_call_butterfly.rs b/examples/examples_strategies/src/bin/strategy_call_butterfly.rs index 9cc08adc..2c8ebcd4 100644 --- a/examples/examples_strategies/src/bin/strategy_call_butterfly.rs +++ b/examples/examples_strategies/src/bin/strategy_call_butterfly.rs @@ -30,7 +30,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.73), // close_fee_short pos_or_panic!(0.73), // close_fee_short pos_or_panic!(0.72), // open_fee_short - ); + )?; let range = strategy.get_range_of_profit().unwrap_or(Positive::ZERO); info!("Title: {}", strategy.get_title()); diff --git a/examples/examples_strategies/src/bin/strategy_custom_complex.rs b/examples/examples_strategies/src/bin/strategy_custom_complex.rs index c43478c6..8f465b94 100644 --- a/examples/examples_strategies/src/bin/strategy_custom_complex.rs +++ b/examples/examples_strategies/src/bin/strategy_custom_complex.rs @@ -124,7 +124,7 @@ fn main() -> Result<(), Error> { Default::default(), 100, Default::default(), - ); + )?; // Display strategy information info!("=== CUSTOM COMPLEX STRATEGY ==="); diff --git a/examples/examples_strategies/src/bin/strategy_custom_dax.rs b/examples/examples_strategies/src/bin/strategy_custom_dax.rs index 401a5862..7dae2e58 100644 --- a/examples/examples_strategies/src/bin/strategy_custom_dax.rs +++ b/examples/examples_strategies/src/bin/strategy_custom_dax.rs @@ -103,7 +103,8 @@ fn main() { Positive::ONE, // Default quantity 1, // days to expiration implied_volatility, - ); + ) + .unwrap(); // Fees are already set in Position::new() above diff --git a/examples/examples_strategies/src/bin/strategy_custom_short_strangle.rs b/examples/examples_strategies/src/bin/strategy_custom_short_strangle.rs index 587f5f8d..ca019ed9 100644 --- a/examples/examples_strategies/src/bin/strategy_custom_short_strangle.rs +++ b/examples/examples_strategies/src/bin/strategy_custom_short_strangle.rs @@ -72,7 +72,7 @@ fn main() -> Result<(), Error> { Default::default(), 100, Default::default(), - ); + )?; // Calculate range between break-even points (if we have at least 2) let range = if strategy.break_even_points.len() >= 2 { diff --git a/examples/examples_strategies/src/bin/strategy_custom_simple.rs b/examples/examples_strategies/src/bin/strategy_custom_simple.rs index 65363776..c7779871 100644 --- a/examples/examples_strategies/src/bin/strategy_custom_simple.rs +++ b/examples/examples_strategies/src/bin/strategy_custom_simple.rs @@ -73,7 +73,7 @@ fn main() -> Result<(), Error> { Default::default(), 50, // Fewer calculation points for simplicity Default::default(), - ); + )?; // Display strategy information info!("=== CUSTOM COVERED CALL STRATEGY ==="); diff --git a/examples/examples_strategies/src/bin/strategy_graph.rs b/examples/examples_strategies/src/bin/strategy_graph.rs index c623a64d..6de64a91 100644 --- a/examples/examples_strategies/src/bin/strategy_graph.rs +++ b/examples/examples_strategies/src/bin/strategy_graph.rs @@ -24,7 +24,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.58), pos_or_panic!(0.55), pos_or_panic!(0.54), - ); + )?; info!("Title: {}", strategy.get_title()); info!("Break Even {:?}", strategy.get_break_even_points()); diff --git a/examples/examples_strategies/src/bin/strategy_iron_butterfly.rs b/examples/examples_strategies/src/bin/strategy_iron_butterfly.rs index 7dfb1088..8835816e 100644 --- a/examples/examples_strategies/src/bin/strategy_iron_butterfly.rs +++ b/examples/examples_strategies/src/bin/strategy_iron_butterfly.rs @@ -22,7 +22,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(16.8), // premium_long_put pos_or_panic!(0.96), // open_fee pos_or_panic!(0.96), // close_fee - ); + )?; if !strategy.validate() { return Err("Invalid strategy".into()); } diff --git a/examples/examples_strategies/src/bin/strategy_iron_condor.rs b/examples/examples_strategies/src/bin/strategy_iron_condor.rs index 2e3a0690..53b42388 100644 --- a/examples/examples_strategies/src/bin/strategy_iron_condor.rs +++ b/examples/examples_strategies/src/bin/strategy_iron_condor.rs @@ -23,7 +23,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(30.7), // premium_long_put pos_or_panic!(0.1), // open_fee pos_or_panic!(0.1), // close_fee - ); + )?; if !strategy.validate() { return Err("Invalid strategy".into()); } diff --git a/examples/examples_strategies/src/bin/strategy_long_butterfly_spread.rs b/examples/examples_strategies/src/bin/strategy_long_butterfly_spread.rs index fc83dacd..3fdc6326 100644 --- a/examples/examples_strategies/src/bin/strategy_long_butterfly_spread.rs +++ b/examples/examples_strategies/src/bin/strategy_long_butterfly_spread.rs @@ -31,7 +31,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.05), // close_fee_long_call_low pos_or_panic!(0.05), // open_fee_long_call_high pos_or_panic!(0.05), // close_fee_long_call_high - ); + )?; info!("Title: {}", strategy.get_title()); info!("Break Even Points: {:?}", strategy.break_even_points); diff --git a/examples/examples_strategies/src/bin/strategy_long_straddle.rs b/examples/examples_strategies/src/bin/strategy_long_straddle.rs index d67a4df7..6cfe5c4a 100644 --- a/examples/examples_strategies/src/bin/strategy_long_straddle.rs +++ b/examples/examples_strategies/src/bin/strategy_long_straddle.rs @@ -20,7 +20,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + )?; let range = strategy.break_even_points[1] - strategy.break_even_points[0]; info!("Title: {}", strategy.get_title()); diff --git a/examples/examples_strategies/src/bin/strategy_long_strangle.rs b/examples/examples_strategies/src/bin/strategy_long_strangle.rs index 5cc148b2..c4a20d03 100644 --- a/examples/examples_strategies/src/bin/strategy_long_strangle.rs +++ b/examples/examples_strategies/src/bin/strategy_long_strangle.rs @@ -21,7 +21,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + )?; let range = strategy.break_even_points[1] - strategy.break_even_points[0]; info!("Title: {}", strategy.get_title()); diff --git a/examples/examples_strategies/src/bin/strategy_poor_mans_covered_call.rs b/examples/examples_strategies/src/bin/strategy_poor_mans_covered_call.rs index 7f8f388e..3c2c445c 100644 --- a/examples/examples_strategies/src/bin/strategy_poor_mans_covered_call.rs +++ b/examples/examples_strategies/src/bin/strategy_poor_mans_covered_call.rs @@ -22,7 +22,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(1.74), // close_fee_short_call pos_or_panic!(0.85), // open_fee_short_put pos_or_panic!(0.85), // close_fee_short_put - ); + )?; info!("Title: {}", strategy.get_title()); info!("Break Even Points: {:?}", strategy.break_even_points); diff --git a/examples/examples_strategies/src/bin/strategy_short_butterfly_spread.rs b/examples/examples_strategies/src/bin/strategy_short_butterfly_spread.rs index a0b33367..4d2e15c2 100644 --- a/examples/examples_strategies/src/bin/strategy_short_butterfly_spread.rs +++ b/examples/examples_strategies/src/bin/strategy_short_butterfly_spread.rs @@ -25,7 +25,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.05), pos_or_panic!(0.05), pos_or_panic!(0.05), - ); + )?; info!("Title: {}", strategy.get_title()); info!("Break Even Points: {:?}", strategy.break_even_points); diff --git a/examples/examples_strategies/src/bin/strategy_short_straddle.rs b/examples/examples_strategies/src/bin/strategy_short_straddle.rs index c61691fa..59e7eadc 100644 --- a/examples/examples_strategies/src/bin/strategy_short_straddle.rs +++ b/examples/examples_strategies/src/bin/strategy_short_straddle.rs @@ -20,7 +20,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + )?; let range = strategy.break_even_points[1] - strategy.break_even_points[0]; info!("Title: {}", strategy.get_title()); diff --git a/examples/examples_strategies/src/bin/strategy_short_strangle.rs b/examples/examples_strategies/src/bin/strategy_short_strangle.rs index 3a9737b4..20247818 100644 --- a/examples/examples_strategies/src/bin/strategy_short_strangle.rs +++ b/examples/examples_strategies/src/bin/strategy_short_strangle.rs @@ -22,7 +22,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + )?; let range = strategy.break_even_points[1] - strategy.break_even_points[0]; info!("Title: {}", strategy.get_title()); diff --git a/examples/examples_strategies/src/bin/strategy_short_strangle_delta_simple.rs b/examples/examples_strategies/src/bin/strategy_short_strangle_delta_simple.rs index 40619a3b..357ba1cc 100644 --- a/examples/examples_strategies/src/bin/strategy_short_strangle_delta_simple.rs +++ b/examples/examples_strategies/src/bin/strategy_short_strangle_delta_simple.rs @@ -22,7 +22,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + )?; let range = strategy.get_range_of_profit().unwrap_or(Positive::ZERO); info!("Title: {}", strategy.get_title()); diff --git a/examples/examples_strategies_best/src/bin/strategy_bear_call_spread_best_area.rs b/examples/examples_strategies_best/src/bin/strategy_bear_call_spread_best_area.rs index 57e105c5..c348d60e 100644 --- a/examples/examples_strategies_best/src/bin/strategy_bear_call_spread_best_area.rs +++ b/examples/examples_strategies_best/src/bin/strategy_bear_call_spread_best_area.rs @@ -21,7 +21,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.82), // close_fee_short_call pos_or_panic!(0.82), // open_fee_short_put pos_or_panic!(0.82), // close_fee_short_put - ); + )?; strategy.get_best_area(&option_chain, FindOptimalSide::Center); debug!("Option Chain: {}", option_chain); debug!("Strategy: {:#?}", strategy); diff --git a/examples/examples_strategies_best/src/bin/strategy_bear_call_spread_best_ratio.rs b/examples/examples_strategies_best/src/bin/strategy_bear_call_spread_best_ratio.rs index 02b9c616..d3ece06d 100644 --- a/examples/examples_strategies_best/src/bin/strategy_bear_call_spread_best_ratio.rs +++ b/examples/examples_strategies_best/src/bin/strategy_bear_call_spread_best_ratio.rs @@ -22,7 +22,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.82), // close_fee_short_call pos_or_panic!(0.82), // open_fee_short_put pos_or_panic!(0.82), // close_fee_short_put - ); + )?; strategy.get_best_ratio(&option_chain, FindOptimalSide::Upper); debug!("Option Chain: {}", option_chain); debug!("Strategy: {:#?}", strategy); diff --git a/examples/examples_strategies_best/src/bin/strategy_bear_put_spread_best_area.rs b/examples/examples_strategies_best/src/bin/strategy_bear_put_spread_best_area.rs index 81b1f943..d8038af8 100644 --- a/examples/examples_strategies_best/src/bin/strategy_bear_put_spread_best_area.rs +++ b/examples/examples_strategies_best/src/bin/strategy_bear_put_spread_best_area.rs @@ -22,7 +22,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.82), // close_fee_short_call pos_or_panic!(0.82), // open_fee_short_put pos_or_panic!(0.82), // close_fee_short_put - ); + )?; strategy.get_best_area(&option_chain, FindOptimalSide::Center); debug!("Option Chain: {}", option_chain); debug!("Strategy: {:#?}", strategy); diff --git a/examples/examples_strategies_best/src/bin/strategy_bear_put_spread_best_ratio.rs b/examples/examples_strategies_best/src/bin/strategy_bear_put_spread_best_ratio.rs index 76dc6731..61afa0d2 100644 --- a/examples/examples_strategies_best/src/bin/strategy_bear_put_spread_best_ratio.rs +++ b/examples/examples_strategies_best/src/bin/strategy_bear_put_spread_best_ratio.rs @@ -22,7 +22,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.82), // close_fee_short_call pos_or_panic!(0.82), // open_fee_short_put pos_or_panic!(0.82), // close_fee_short_put - ); + )?; strategy.get_best_ratio(&option_chain, FindOptimalSide::Upper); debug!("Option Chain: {}", option_chain); debug!("Strategy: {:#?}", strategy); diff --git a/examples/examples_strategies_best/src/bin/strategy_bull_call_spread_best_area.rs b/examples/examples_strategies_best/src/bin/strategy_bull_call_spread_best_area.rs index 260213d0..49e9fe61 100644 --- a/examples/examples_strategies_best/src/bin/strategy_bull_call_spread_best_area.rs +++ b/examples/examples_strategies_best/src/bin/strategy_bull_call_spread_best_area.rs @@ -22,7 +22,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.82), // close_fee_short_call pos_or_panic!(0.82), // open_fee_short_put pos_or_panic!(0.82), // close_fee_short_put - ); + )?; strategy.get_best_area(&option_chain, FindOptimalSide::Center); debug!("Option Chain: {}", option_chain); debug!("Strategy: {:#?}", strategy); diff --git a/examples/examples_strategies_best/src/bin/strategy_bull_call_spread_best_ratio.rs b/examples/examples_strategies_best/src/bin/strategy_bull_call_spread_best_ratio.rs index e8023f4a..fd8306fa 100644 --- a/examples/examples_strategies_best/src/bin/strategy_bull_call_spread_best_ratio.rs +++ b/examples/examples_strategies_best/src/bin/strategy_bull_call_spread_best_ratio.rs @@ -22,7 +22,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.82), // close_fee_short_call pos_or_panic!(0.82), // open_fee_short_put pos_or_panic!(0.82), // close_fee_short_put - ); + )?; strategy.get_best_ratio(&option_chain, FindOptimalSide::All); debug!("Option Chain: {}", option_chain); debug!("Strategy: {:#?}", strategy); diff --git a/examples/examples_strategies_best/src/bin/strategy_bull_put_spread_best_area.rs b/examples/examples_strategies_best/src/bin/strategy_bull_put_spread_best_area.rs index 1b2e8fb5..da480ce0 100644 --- a/examples/examples_strategies_best/src/bin/strategy_bull_put_spread_best_area.rs +++ b/examples/examples_strategies_best/src/bin/strategy_bull_put_spread_best_area.rs @@ -22,7 +22,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.81), // close_fee_short_call pos_or_panic!(0.82), // open_fee_short_put pos_or_panic!(0.82), // close_fee_short_put - ); + )?; strategy.get_best_area(&option_chain, FindOptimalSide::Center); debug!("Option Chain: {}", option_chain); debug!("Strategy: {:#?}", strategy); diff --git a/examples/examples_strategies_best/src/bin/strategy_bull_put_spread_best_ratio.rs b/examples/examples_strategies_best/src/bin/strategy_bull_put_spread_best_ratio.rs index 536fb3d3..0f099689 100644 --- a/examples/examples_strategies_best/src/bin/strategy_bull_put_spread_best_ratio.rs +++ b/examples/examples_strategies_best/src/bin/strategy_bull_put_spread_best_ratio.rs @@ -22,7 +22,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.82), // close_fee_short_call pos_or_panic!(0.82), // open_fee_short_put pos_or_panic!(0.82), // close_fee_short_put - ); + )?; strategy.get_best_area(&option_chain, FindOptimalSide::Lower); debug!("Option Chain: {}", option_chain); debug!("Strategy: {:#?}", strategy); diff --git a/examples/examples_strategies_best/src/bin/strategy_call_butterfly_best_area.rs b/examples/examples_strategies_best/src/bin/strategy_call_butterfly_best_area.rs index 0f5976e0..1bc5165a 100644 --- a/examples/examples_strategies_best/src/bin/strategy_call_butterfly_best_area.rs +++ b/examples/examples_strategies_best/src/bin/strategy_call_butterfly_best_area.rs @@ -31,7 +31,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.95), // close_fee_short_low pos_or_panic!(0.95), // open_fee_short_high pos_or_panic!(0.95), // close_fee_short_high - ); + )?; strategy.get_best_area(&option_chain, FindOptimalSide::Center); let range = strategy.get_range_of_profit().unwrap_or(Positive::ZERO); diff --git a/examples/examples_strategies_best/src/bin/strategy_call_butterfly_best_ratio.rs b/examples/examples_strategies_best/src/bin/strategy_call_butterfly_best_ratio.rs index ffad46e7..0f55ae7f 100644 --- a/examples/examples_strategies_best/src/bin/strategy_call_butterfly_best_ratio.rs +++ b/examples/examples_strategies_best/src/bin/strategy_call_butterfly_best_ratio.rs @@ -31,7 +31,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.73), // close_fee_short pos_or_panic!(0.73), // close_fee_short pos_or_panic!(0.73), - ); + )?; strategy.get_best_ratio( &option_chain, diff --git a/examples/examples_strategies_best/src/bin/strategy_iron_butterfly_best_area.rs b/examples/examples_strategies_best/src/bin/strategy_iron_butterfly_best_area.rs index 6995ff18..70be325a 100644 --- a/examples/examples_strategies_best/src/bin/strategy_iron_butterfly_best_area.rs +++ b/examples/examples_strategies_best/src/bin/strategy_iron_butterfly_best_area.rs @@ -24,7 +24,7 @@ fn main() -> Result<(), Error> { Positive::ZERO, // premium_long_put Positive::ONE, // open_fee Positive::ONE, // close_fee - ); + )?; strategy.get_best_area(&option_chain, FindOptimalSide::Lower); debug!("Option Chain: {}", option_chain); diff --git a/examples/examples_strategies_best/src/bin/strategy_iron_butterfly_best_ratio.rs b/examples/examples_strategies_best/src/bin/strategy_iron_butterfly_best_ratio.rs index 47569255..3f4047d1 100644 --- a/examples/examples_strategies_best/src/bin/strategy_iron_butterfly_best_ratio.rs +++ b/examples/examples_strategies_best/src/bin/strategy_iron_butterfly_best_ratio.rs @@ -24,7 +24,7 @@ fn main() -> Result<(), Error> { Positive::ZERO, // premium_long_put Positive::ONE, // open_fee Positive::ONE, // close_fee - ); + )?; strategy.get_best_ratio(&option_chain, FindOptimalSide::All); debug!("Option Chain: {}", option_chain); diff --git a/examples/examples_strategies_best/src/bin/strategy_iron_condor_best_area.rs b/examples/examples_strategies_best/src/bin/strategy_iron_condor_best_area.rs index 49a94b4f..68b1f4d9 100644 --- a/examples/examples_strategies_best/src/bin/strategy_iron_condor_best_area.rs +++ b/examples/examples_strategies_best/src/bin/strategy_iron_condor_best_area.rs @@ -25,7 +25,7 @@ fn main() -> Result<(), Error> { Positive::ZERO, // premium_long_put Positive::ONE, // open_fee Positive::ONE, // close_fee - ); + )?; strategy.get_best_area(&option_chain, FindOptimalSide::All); debug!("Option Chain: {}", option_chain); diff --git a/examples/examples_strategies_best/src/bin/strategy_iron_condor_best_ratio.rs b/examples/examples_strategies_best/src/bin/strategy_iron_condor_best_ratio.rs index 26cedf97..15f24eb9 100644 --- a/examples/examples_strategies_best/src/bin/strategy_iron_condor_best_ratio.rs +++ b/examples/examples_strategies_best/src/bin/strategy_iron_condor_best_ratio.rs @@ -25,7 +25,7 @@ fn main() -> Result<(), Error> { Positive::ZERO, // premium_long_put Positive::ONE, // open_fee Positive::ONE, // close_fee - ); + )?; strategy.get_best_ratio(&option_chain, FindOptimalSide::All); debug!("Option Chain: {}", option_chain); diff --git a/examples/examples_strategies_best/src/bin/strategy_long_butterfly_spread_best_area.rs b/examples/examples_strategies_best/src/bin/strategy_long_butterfly_spread_best_area.rs index 95f07553..7efd689a 100644 --- a/examples/examples_strategies_best/src/bin/strategy_long_butterfly_spread_best_area.rs +++ b/examples/examples_strategies_best/src/bin/strategy_long_butterfly_spread_best_area.rs @@ -26,7 +26,7 @@ fn main() -> Result<(), Error> { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + )?; strategy.get_best_area(&option_chain, FindOptimalSide::All); debug!("Option Chain: {}", option_chain); debug!("Strategy: {:#?}", strategy); diff --git a/examples/examples_strategies_best/src/bin/strategy_long_butterfly_spread_best_ratio.rs b/examples/examples_strategies_best/src/bin/strategy_long_butterfly_spread_best_ratio.rs index aee19c25..a7e0b9e5 100644 --- a/examples/examples_strategies_best/src/bin/strategy_long_butterfly_spread_best_ratio.rs +++ b/examples/examples_strategies_best/src/bin/strategy_long_butterfly_spread_best_ratio.rs @@ -26,7 +26,7 @@ fn main() -> Result<(), Error> { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + )?; strategy.get_best_ratio(&option_chain, FindOptimalSide::All); debug!("Option Chain: {}", option_chain); debug!("Strategy: {:#?}", strategy); diff --git a/examples/examples_strategies_best/src/bin/strategy_long_straddle_best_area.rs b/examples/examples_strategies_best/src/bin/strategy_long_straddle_best_area.rs index 11b8ca2b..395ddf30 100644 --- a/examples/examples_strategies_best/src/bin/strategy_long_straddle_best_area.rs +++ b/examples/examples_strategies_best/src/bin/strategy_long_straddle_best_area.rs @@ -21,7 +21,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.82), // close_fee_short_call pos_or_panic!(0.82), // open_fee_short_put pos_or_panic!(0.82), // close_fee_short_put - ); + )?; strategy.get_best_area(&option_chain, FindOptimalSide::All); debug!("Strategy: {:#?}", strategy); let range = strategy.get_range_of_profit().unwrap_or(Positive::ZERO); diff --git a/examples/examples_strategies_best/src/bin/strategy_long_straddle_best_ratio.rs b/examples/examples_strategies_best/src/bin/strategy_long_straddle_best_ratio.rs index f7398751..1a82a027 100644 --- a/examples/examples_strategies_best/src/bin/strategy_long_straddle_best_ratio.rs +++ b/examples/examples_strategies_best/src/bin/strategy_long_straddle_best_ratio.rs @@ -21,7 +21,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.82), // close_fee_short_call pos_or_panic!(0.82), // open_fee_short_put pos_or_panic!(0.82), // close_fee_short_put - ); + )?; strategy.get_best_ratio(&option_chain, FindOptimalSide::All); debug!("Strategy: {:#?}", strategy); let range = strategy.get_range_of_profit().unwrap_or(Positive::ZERO); diff --git a/examples/examples_strategies_best/src/bin/strategy_long_strangle_best_area.rs b/examples/examples_strategies_best/src/bin/strategy_long_strangle_best_area.rs index 9f04112e..2d68745f 100644 --- a/examples/examples_strategies_best/src/bin/strategy_long_strangle_best_area.rs +++ b/examples/examples_strategies_best/src/bin/strategy_long_strangle_best_area.rs @@ -22,7 +22,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.82), // close_fee_short_call pos_or_panic!(0.82), // open_fee_short_put pos_or_panic!(0.82), // close_fee_short_put - ); + )?; strategy.get_best_area(&option_chain, FindOptimalSide::Center); debug!("Strategy: {:#?}", strategy); let range = strategy.get_range_of_profit().unwrap_or(Positive::ZERO); diff --git a/examples/examples_strategies_best/src/bin/strategy_long_strangle_best_ratio.rs b/examples/examples_strategies_best/src/bin/strategy_long_strangle_best_ratio.rs index 4ccb843a..b274d19c 100644 --- a/examples/examples_strategies_best/src/bin/strategy_long_strangle_best_ratio.rs +++ b/examples/examples_strategies_best/src/bin/strategy_long_strangle_best_ratio.rs @@ -22,7 +22,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.82), // close_fee_short_call pos_or_panic!(0.82), // open_fee_short_put pos_or_panic!(0.82), // close_fee_short_put - ); + )?; strategy.get_best_ratio(&option_chain, FindOptimalSide::All); debug!("Strategy: {:#?}", strategy); let range = strategy.get_range_of_profit().unwrap_or(Positive::ZERO); diff --git a/examples/examples_strategies_best/src/bin/strategy_poor_mans_covered_call_best_area.rs b/examples/examples_strategies_best/src/bin/strategy_poor_mans_covered_call_best_area.rs index 6e134713..a60aae54 100644 --- a/examples/examples_strategies_best/src/bin/strategy_poor_mans_covered_call_best_area.rs +++ b/examples/examples_strategies_best/src/bin/strategy_poor_mans_covered_call_best_area.rs @@ -23,7 +23,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(1.74), // close_fee_short_call pos_or_panic!(0.85), // open_fee_short_put pos_or_panic!(0.85), // close_fee_short_put - ); + )?; strategy.get_best_area(&option_chain, FindOptimalSide::Center); debug!("Strategy: {:#?}", strategy); let range = strategy.get_range_of_profit().unwrap_or(Positive::ZERO); diff --git a/examples/examples_strategies_best/src/bin/strategy_poor_mans_covered_call_best_ratio.rs b/examples/examples_strategies_best/src/bin/strategy_poor_mans_covered_call_best_ratio.rs index baeb5dac..19e247ed 100644 --- a/examples/examples_strategies_best/src/bin/strategy_poor_mans_covered_call_best_ratio.rs +++ b/examples/examples_strategies_best/src/bin/strategy_poor_mans_covered_call_best_ratio.rs @@ -24,7 +24,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(1.74), // close_fee_short_call pos_or_panic!(0.85), // open_fee_short_put pos_or_panic!(0.85), // close_fee_short_put - ); + )?; strategy.get_best_ratio(&option_chain, FindOptimalSide::Upper); debug!("Option Chain: {}", option_chain); diff --git a/examples/examples_strategies_best/src/bin/strategy_short_butterfly_spread_best_area.rs b/examples/examples_strategies_best/src/bin/strategy_short_butterfly_spread_best_area.rs index 57cab376..df226df6 100644 --- a/examples/examples_strategies_best/src/bin/strategy_short_butterfly_spread_best_area.rs +++ b/examples/examples_strategies_best/src/bin/strategy_short_butterfly_spread_best_area.rs @@ -26,7 +26,7 @@ fn main() -> Result<(), Error> { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + )?; strategy.get_best_area(&option_chain, FindOptimalSide::All); debug!("Option Chain: {}", option_chain); debug!("Strategy: {:#?}", strategy); diff --git a/examples/examples_strategies_best/src/bin/strategy_short_butterfly_spread_best_ratio.rs b/examples/examples_strategies_best/src/bin/strategy_short_butterfly_spread_best_ratio.rs index 408d3353..b7b73445 100644 --- a/examples/examples_strategies_best/src/bin/strategy_short_butterfly_spread_best_ratio.rs +++ b/examples/examples_strategies_best/src/bin/strategy_short_butterfly_spread_best_ratio.rs @@ -26,7 +26,7 @@ fn main() -> Result<(), Error> { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + )?; strategy.get_best_ratio(&option_chain, FindOptimalSide::All); debug!("Option Chain: {}", option_chain); debug!("Strategy: {:#?}", strategy); diff --git a/examples/examples_strategies_best/src/bin/strategy_short_straddle_best_area.rs b/examples/examples_strategies_best/src/bin/strategy_short_straddle_best_area.rs index 48590c7a..6233609e 100644 --- a/examples/examples_strategies_best/src/bin/strategy_short_straddle_best_area.rs +++ b/examples/examples_strategies_best/src/bin/strategy_short_straddle_best_area.rs @@ -21,7 +21,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.82), // close_fee_short_call pos_or_panic!(0.82), // open_fee_short_put pos_or_panic!(0.82), // close_fee_short_put - ); + )?; // strategy.best_area(&option_chain, FindOptimalSide::Range(pos_or_panic!(5700.0), pos_or_panic!(6100.0))); strategy.get_best_area(&option_chain, FindOptimalSide::Upper); debug!("Strategy: {:#?}", strategy); diff --git a/examples/examples_strategies_best/src/bin/strategy_short_straddle_best_ratio.rs b/examples/examples_strategies_best/src/bin/strategy_short_straddle_best_ratio.rs index a11ce621..fc49197f 100644 --- a/examples/examples_strategies_best/src/bin/strategy_short_straddle_best_ratio.rs +++ b/examples/examples_strategies_best/src/bin/strategy_short_straddle_best_ratio.rs @@ -21,7 +21,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.82), // close_fee_short_call pos_or_panic!(0.82), // open_fee_short_put pos_or_panic!(0.82), // close_fee_short_put - ); + )?; strategy.get_best_ratio(&option_chain, FindOptimalSide::Upper); debug!("Option Chain: {}", option_chain); debug!("Strategy: {:#?}", strategy); diff --git a/examples/examples_strategies_best/src/bin/strategy_short_strangle_best_area.rs b/examples/examples_strategies_best/src/bin/strategy_short_strangle_best_area.rs index feb14742..d21e0fdc 100644 --- a/examples/examples_strategies_best/src/bin/strategy_short_strangle_best_area.rs +++ b/examples/examples_strategies_best/src/bin/strategy_short_strangle_best_area.rs @@ -30,7 +30,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.12), // close_fee_short_call pos_or_panic!(0.12), // open_fee_short_put pos_or_panic!(0.12), // close_fee_short_put - ); + )?; strategy.get_best_area(&option_chain, FindOptimalSide::Deltable(pos_or_panic!(0.3))); info!("Strategy: {:#?}", strategy); let range = strategy.get_range_of_profit().unwrap_or(Positive::ZERO); diff --git a/examples/examples_strategies_best/src/bin/strategy_short_strangle_best_ratio.rs b/examples/examples_strategies_best/src/bin/strategy_short_strangle_best_ratio.rs index 8bb02b9b..22d12dfe 100644 --- a/examples/examples_strategies_best/src/bin/strategy_short_strangle_best_ratio.rs +++ b/examples/examples_strategies_best/src/bin/strategy_short_strangle_best_ratio.rs @@ -25,7 +25,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.82), // close_fee_short_call pos_or_panic!(0.82), // open_fee_short_put pos_or_panic!(0.82), // close_fee_short_put - ); + )?; strategy.get_best_ratio(&option_chain, FindOptimalSide::Upper); debug!("Option Chain: {}", option_chain); debug!("Strategy: {:#?}", strategy); diff --git a/examples/examples_strategies_delta/src/bin/strategy_bear_call_spread_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_bear_call_spread_delta.rs index 08b67f24..de5e64f5 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_bear_call_spread_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_bear_call_spread_delta.rs @@ -26,7 +26,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.78), // open_fee_long pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short - ); + )?; info!("Title: {}", strategy.get_title()); info!("Break Even Points: {:?}", strategy.break_even_points); diff --git a/examples/examples_strategies_delta/src/bin/strategy_bear_call_spread_extended_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_bear_call_spread_extended_delta.rs index fc394439..b8399a20 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_bear_call_spread_extended_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_bear_call_spread_extended_delta.rs @@ -31,7 +31,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.78), pos_or_panic!(0.73), pos_or_panic!(0.73), - ); + )?; info!("=== BearCallSpread Extended Delta Analysis ==="); info!("Title: {}", strategy.get_title()); diff --git a/examples/examples_strategies_delta/src/bin/strategy_bear_put_spread_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_bear_put_spread_delta.rs index 00703b36..db9fe997 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_bear_put_spread_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_bear_put_spread_delta.rs @@ -26,7 +26,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.78), // open_fee_long pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short - ); + )?; info!("Title: {}", strategy.get_title()); info!("Break Even Points: {:?}", strategy.break_even_points); diff --git a/examples/examples_strategies_delta/src/bin/strategy_bear_put_spread_extended_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_bear_put_spread_extended_delta.rs index 9511a705..d9c2cbbb 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_bear_put_spread_extended_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_bear_put_spread_extended_delta.rs @@ -31,7 +31,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.78), pos_or_panic!(0.73), pos_or_panic!(0.73), - ); + )?; info!("=== BearPutSpread Extended Delta Analysis ==="); info!("Title: {}", strategy.get_title()); diff --git a/examples/examples_strategies_delta/src/bin/strategy_bull_call_spread_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_bull_call_spread_delta.rs index e530a062..33f24b5a 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_bull_call_spread_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_bull_call_spread_delta.rs @@ -26,7 +26,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.78), // open_fee_long pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short - ); + )?; info!("Title: {}", strategy.get_title()); info!("Break Even Points: {:?}", strategy.break_even_points); diff --git a/examples/examples_strategies_delta/src/bin/strategy_bull_call_spread_extended_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_bull_call_spread_extended_delta.rs index 73b1d181..8d2e74d3 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_bull_call_spread_extended_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_bull_call_spread_extended_delta.rs @@ -31,7 +31,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.78), pos_or_panic!(0.73), pos_or_panic!(0.73), - ); + )?; info!("=== BullCallSpread Extended Delta Analysis ==="); info!("Title: {}", strategy.get_title()); diff --git a/examples/examples_strategies_delta/src/bin/strategy_bull_put_spread_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_bull_put_spread_delta.rs index c5cfe073..af9817d3 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_bull_put_spread_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_bull_put_spread_delta.rs @@ -26,7 +26,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.78), // open_fee_long pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short - ); + )?; info!("Title: {}", strategy.get_title()); info!("Break Even Points: {:?}", strategy.break_even_points); diff --git a/examples/examples_strategies_delta/src/bin/strategy_bull_put_spread_extended_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_bull_put_spread_extended_delta.rs index b5791394..78b5324a 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_bull_put_spread_extended_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_bull_put_spread_extended_delta.rs @@ -31,7 +31,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.78), pos_or_panic!(0.73), pos_or_panic!(0.73), - ); + )?; info!("=== BullPutSpread Extended Delta Analysis ==="); info!("Title: {}", strategy.get_title()); diff --git a/examples/examples_strategies_delta/src/bin/strategy_call_butterfly_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_call_butterfly_delta.rs index 7217d4ca..b67f1b12 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_call_butterfly_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_call_butterfly_delta.rs @@ -25,7 +25,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.73), // close_fee_short pos_or_panic!(0.73), // close_fee_short pos_or_panic!(0.73), - ); + )?; info!("Title: {}", strategy.get_title()); info!("Break Even Points: {:?}", strategy.break_even_points); diff --git a/examples/examples_strategies_delta/src/bin/strategy_call_butterfly_extended_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_call_butterfly_extended_delta.rs index 2ccee398..a6d39de3 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_call_butterfly_extended_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_call_butterfly_extended_delta.rs @@ -35,7 +35,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.73), pos_or_panic!(0.78), pos_or_panic!(0.73), - ); + )?; info!("=== CallButterfly Extended Delta Analysis ==="); info!("Title: {}", strategy.get_title()); diff --git a/examples/examples_strategies_delta/src/bin/strategy_iron_butterfly_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_iron_butterfly_delta.rs index bd7eb0f8..c0818c85 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_iron_butterfly_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_iron_butterfly_delta.rs @@ -22,7 +22,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(16.8), // premium_long_put pos_or_panic!(0.96), // open_fee pos_or_panic!(0.96), // close_fee - ); + )?; info!("Title: {}", strategy.get_title()); info!("Break Even Points: {:?}", strategy.break_even_points); diff --git a/examples/examples_strategies_delta/src/bin/strategy_iron_butterfly_extended_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_iron_butterfly_extended_delta.rs index 129a7a2b..e896d580 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_iron_butterfly_extended_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_iron_butterfly_extended_delta.rs @@ -32,7 +32,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(16.0), pos_or_panic!(0.96), pos_or_panic!(0.96), - ); + )?; info!("=== IronButterfly Extended Delta Analysis ==="); info!("Title: {}", strategy.get_title()); diff --git a/examples/examples_strategies_delta/src/bin/strategy_iron_condor_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_iron_condor_delta.rs index 895d9f47..5a7e6a40 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_iron_condor_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_iron_condor_delta.rs @@ -22,7 +22,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(16.8), // premium_long_put pos_or_panic!(0.96), // open_fee pos_or_panic!(0.96), // close_fee - ); + )?; info!("Title: {}", strategy.get_title()); info!("Break Even Points: {:?}", strategy.break_even_points); diff --git a/examples/examples_strategies_delta/src/bin/strategy_iron_condor_extended_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_iron_condor_extended_delta.rs index e162a351..3c074d02 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_iron_condor_extended_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_iron_condor_extended_delta.rs @@ -33,7 +33,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(16.8), pos_or_panic!(0.96), pos_or_panic!(0.96), - ); + )?; info!("=== IronCondor Extended Delta Analysis ==="); info!("Title: {}", strategy.get_title()); diff --git a/examples/examples_strategies_delta/src/bin/strategy_long_butterfly_spread_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_long_butterfly_spread_delta.rs index a92a9fb7..ac5da1a3 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_long_butterfly_spread_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_long_butterfly_spread_delta.rs @@ -25,7 +25,7 @@ fn main() -> Result<(), Error> { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + )?; info!("Title: {}", strategy.get_title()); info!("Break Even Points: {:?}", strategy.break_even_points); diff --git a/examples/examples_strategies_delta/src/bin/strategy_long_butterfly_spread_extended_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_long_butterfly_spread_extended_delta.rs index c49290ea..8d83814c 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_long_butterfly_spread_extended_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_long_butterfly_spread_extended_delta.rs @@ -35,7 +35,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.73), pos_or_panic!(0.78), pos_or_panic!(0.73), - ); + )?; info!("=== LongButterflySpread Extended Delta Analysis ==="); info!("Title: {}", strategy.get_title()); diff --git a/examples/examples_strategies_delta/src/bin/strategy_long_straddle_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_long_straddle_delta.rs index 19dc7fe7..b5a8b172 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_long_straddle_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_long_straddle_delta.rs @@ -19,7 +19,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + )?; info!("Title: {}", strategy.get_title()); info!("Break Even Points: {:?}", strategy.break_even_points); diff --git a/examples/examples_strategies_delta/src/bin/strategy_long_straddle_extended_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_long_straddle_extended_delta.rs index ee73791c..b9976bac 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_long_straddle_extended_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_long_straddle_extended_delta.rs @@ -30,7 +30,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.78), pos_or_panic!(0.73), pos_or_panic!(0.73), - ); + )?; info!("=== LongStraddle Extended Delta Analysis ==="); info!("Title: {}", strategy.get_title()); diff --git a/examples/examples_strategies_delta/src/bin/strategy_long_strangle_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_long_strangle_delta.rs index a02b976b..d4b9ec09 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_long_strangle_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_long_strangle_delta.rs @@ -21,7 +21,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + )?; // let price_range = strategy.best_range_to_show(Positive::ONE).unwrap(); let range = strategy.break_even_points[1] - strategy.break_even_points[0]; diff --git a/examples/examples_strategies_delta/src/bin/strategy_long_strangle_extended_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_long_strangle_extended_delta.rs index e33b171c..ebe62cda 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_long_strangle_extended_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_long_strangle_extended_delta.rs @@ -31,7 +31,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.78), pos_or_panic!(0.73), pos_or_panic!(0.73), - ); + )?; info!("=== LongStrangle Extended Delta Analysis ==="); info!("Title: {}", strategy.get_title()); diff --git a/examples/examples_strategies_delta/src/bin/strategy_pmcc_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_pmcc_delta.rs index abbbe6ba..be1fe308 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_pmcc_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_pmcc_delta.rs @@ -22,7 +22,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + )?; info!("Title: {}", strategy.get_title()); info!("Break Even Points: {:?}", strategy.break_even_points); diff --git a/examples/examples_strategies_delta/src/bin/strategy_pmcc_extended_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_pmcc_extended_delta.rs index e68b2591..833fad93 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_pmcc_extended_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_pmcc_extended_delta.rs @@ -32,7 +32,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.78), pos_or_panic!(0.73), pos_or_panic!(0.73), - ); + )?; info!("=== PoorMansCoveredCall Extended Delta Analysis ==="); info!("Title: {}", strategy.get_title()); diff --git a/examples/examples_strategies_delta/src/bin/strategy_short_butterfly_spread_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_short_butterfly_spread_delta.rs index 25e915fe..1782fa7d 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_short_butterfly_spread_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_short_butterfly_spread_delta.rs @@ -25,7 +25,7 @@ fn main() -> Result<(), Error> { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + )?; info!("Title: {}", strategy.get_title()); info!("Break Even Points: {:?}", strategy.break_even_points); diff --git a/examples/examples_strategies_delta/src/bin/strategy_short_butterfly_spread_extended_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_short_butterfly_spread_extended_delta.rs index e5f3ff32..cefbaefc 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_short_butterfly_spread_extended_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_short_butterfly_spread_extended_delta.rs @@ -35,7 +35,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.73), pos_or_panic!(0.78), pos_or_panic!(0.73), - ); + )?; info!("=== ShortButterflySpread Extended Delta Analysis ==="); info!("Title: {}", strategy.get_title()); diff --git a/examples/examples_strategies_delta/src/bin/strategy_short_straddle_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_short_straddle_delta.rs index 03415db1..038b41a5 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_short_straddle_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_short_straddle_delta.rs @@ -20,7 +20,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + )?; let range = strategy.break_even_points[1] - strategy.break_even_points[0]; info!("Title: {}", strategy.get_title()); diff --git a/examples/examples_strategies_delta/src/bin/strategy_short_straddle_extended_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_short_straddle_extended_delta.rs index 871da13c..5df6d579 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_short_straddle_extended_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_short_straddle_extended_delta.rs @@ -30,7 +30,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(0.78), pos_or_panic!(0.73), pos_or_panic!(0.73), - ); + )?; info!("=== ShortStraddle Extended Delta Analysis ==="); info!("Title: {}", strategy.get_title()); diff --git a/examples/examples_strategies_delta/src/bin/strategy_short_strangle_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_short_strangle_delta.rs index 2c444c49..2e6b8eb5 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_short_strangle_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_short_strangle_delta.rs @@ -22,7 +22,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + )?; // let price_range = strategy.best_range_to_show(Positive::ONE).unwrap(); let range = strategy.break_even_points[1] - strategy.break_even_points[0]; diff --git a/examples/examples_strategies_delta/src/bin/strategy_short_strangle_extended_delta.rs b/examples/examples_strategies_delta/src/bin/strategy_short_strangle_extended_delta.rs index 1a472463..dff4aaec 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_short_strangle_extended_delta.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_short_strangle_extended_delta.rs @@ -32,7 +32,7 @@ fn main() -> Result<(), Error> { pos_or_panic!(7.01), pos_or_panic!(7.01), pos_or_panic!(7.01), - ); + )?; info!("=== ShortStrangle Extended Delta Analysis ==="); info!("Title: {}", strategy.get_title()); diff --git a/src/error/probability.rs b/src/error/probability.rs index f3d4a165..22ce1fd0 100644 --- a/src/error/probability.rs +++ b/src/error/probability.rs @@ -462,6 +462,15 @@ impl From for ProbabilityError { } StrategyError::GreeksError(err) => ProbabilityError::StdError(err.to_string()), StrategyError::PositiveError(err) => ProbabilityError::StdError(err.to_string()), + StrategyError::NumericConversion { value } => ProbabilityError::StdError(format!( + "numeric conversion failed: {value} is not a finite Decimal" + )), + StrategyError::MissingGreek { name } => { + ProbabilityError::StdError(format!("missing greek `{name}`")) + } + StrategyError::EmptyCollection { context } => { + ProbabilityError::StdError(format!("empty collection: {context}")) + } } } } diff --git a/src/error/strategies.rs b/src/error/strategies.rs index e5e88d35..87548d64 100644 --- a/src/error/strategies.rs +++ b/src/error/strategies.rs @@ -128,6 +128,38 @@ pub enum StrategyError { /// Positive value errors #[error(transparent)] PositiveError(#[from] positive::PositiveError), + + /// Numeric conversion failed at the `f64` ↔ `Decimal` boundary. + /// + /// Raised when a non-finite (`NaN` / `±Inf`) `f64` cannot be represented + /// as a `Decimal`, or when a representation overflow occurs. + #[error("numeric conversion failed: {value} is not a finite Decimal")] + NumericConversion { + /// The offending `f64` value that could not be converted. + value: f64, + }, + + /// A required greek value was missing from an option. + /// + /// Raised when a strategy expects a greek (delta, gamma, vega, theta, + /// rho) to be present on an option (e.g., for delta-neutral validation) + /// but the option's greek calculation returned `None`. + #[error("missing greek `{name}`: option not initialized for greek calculation")] + MissingGreek { + /// Name of the greek (`"delta"`, `"gamma"`, `"vega"`, `"theta"`, `"rho"`). + name: &'static str, + }, + + /// A required collection was empty when at least one element was expected. + /// + /// Raised when a strategy operation needs to pick an element out of a + /// collection (option chain, break-even points, profit ranges) and the + /// collection turned out to be empty. + #[error("empty collection in strategy operation: {context}")] + EmptyCollection { + /// Description of where the empty collection was encountered. + context: String, + }, } /// Represents different types of errors that can occur during price-related operations. @@ -338,6 +370,45 @@ impl StrategyError { reason: reason.to_string(), }) } + + /// Builds a `NumericConversion` error for an `f64` that could not be + /// converted to a `Decimal` (typically `NaN` / `Inf` or overflow). + /// + /// # Errors + /// + /// This is an error constructor — it always returns the variant. + #[cold] + #[inline(never)] + #[must_use] + pub fn numeric_conversion(value: f64) -> Self { + StrategyError::NumericConversion { value } + } + + /// Builds a `MissingGreek` error for an option missing a required greek. + /// + /// # Errors + /// + /// This is an error constructor — it always returns the variant. + #[cold] + #[inline(never)] + #[must_use] + pub fn missing_greek(name: &'static str) -> Self { + StrategyError::MissingGreek { name } + } + + /// Builds an `EmptyCollection` error for an unexpectedly empty collection. + /// + /// # Errors + /// + /// This is an error constructor — it always returns the variant. + #[cold] + #[inline(never)] + #[must_use] + pub fn empty_collection(context: impl Into) -> Self { + StrategyError::EmptyCollection { + context: context.into(), + } + } } impl From for StrategyError { @@ -440,6 +511,60 @@ mod tests { } } +#[cfg(test)] +mod tests_panic_free_variants { + use super::*; + + #[test] + fn test_numeric_conversion_constructor_and_display() { + let err = StrategyError::numeric_conversion(f64::NAN); + assert!(matches!(err, StrategyError::NumericConversion { .. })); + assert!(err.to_string().contains("NaN")); + } + + #[test] + fn test_numeric_conversion_inf_message_includes_value() { + let err = StrategyError::numeric_conversion(f64::INFINITY); + assert!(err.to_string().contains("inf")); + } + + #[test] + fn test_missing_greek_constructor() { + let err = StrategyError::missing_greek("delta"); + match err { + StrategyError::MissingGreek { name } => assert_eq!(name, "delta"), + other => panic!("expected MissingGreek, got {other:?}"), + } + } + + #[test] + fn test_missing_greek_display() { + let err = StrategyError::missing_greek("vega"); + assert!(err.to_string().contains("missing greek `vega`")); + } + + #[test] + fn test_empty_collection_constructor_accepts_str_and_string() { + let from_str = StrategyError::empty_collection("option chain"); + let from_string = StrategyError::empty_collection(String::from("break-even points")); + assert!(matches!(from_str, StrategyError::EmptyCollection { .. })); + assert!(matches!(from_string, StrategyError::EmptyCollection { .. })); + assert!(from_str.to_string().contains("option chain")); + assert!(from_string.to_string().contains("break-even points")); + } + + #[test] + fn test_panic_free_variants_route_through_probability_error() { + use crate::error::ProbabilityError; + let nc = ProbabilityError::from(StrategyError::numeric_conversion(f64::NAN)); + let mg = ProbabilityError::from(StrategyError::missing_greek("delta")); + let ec = ProbabilityError::from(StrategyError::empty_collection("chain")); + assert!(nc.to_string().contains("numeric conversion")); + assert!(mg.to_string().contains("missing greek")); + assert!(ec.to_string().contains("empty collection")); + } +} + #[cfg(test)] mod tests_display { use super::*; diff --git a/src/lib.rs b/src/lib.rs index 82df6d53..7be6cc05 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ #![allow(unknown_lints)] #![allow(clippy::literal_string_with_formatting_args)] -//! # OptionStratLib v0.15.3: Financial Options Library +//! # OptionStratLib v0.16.0: Financial Options Library //! //! ## Table of Contents //! 1. [Introduction](#introduction) @@ -663,7 +663,7 @@ //! //! ```toml //! [dependencies] -//! optionstratlib = "0.15.3" +//! optionstratlib = "0.16.0" //! ``` //! //! Or use cargo to add it to your project: @@ -678,7 +678,7 @@ //! //! ```toml //! [dependencies] -//! optionstratlib = { version = "0.15.3", features = ["plotly"] } +//! optionstratlib = { version = "0.16.0", features = ["plotly"] } //! ``` //! //! - `plotly`: Enables interactive visualization using plotly.rs @@ -789,7 +789,7 @@ //! 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()); @@ -920,7 +920,7 @@ //! Positive::ONE, //! 30, //! implied_volatility, -//! ); +//! ).expect("valid custom strategy"); //! //! tracing::info!("Strategy created: {}", strategy.get_title()); //! ``` @@ -1028,7 +1028,7 @@ //! //! --- //! -//! **OptionStratLib v0.15.3** - Built with ❤️ in Rust for the financial community +//! **OptionStratLib v0.16.0** - Built with ❤️ in Rust for the financial community //! /// # OptionsStratLib: Financial Options Trading Library diff --git a/src/strategies/base.rs b/src/strategies/base.rs index 08420c24..4607f8f4 100644 --- a/src/strategies/base.rs +++ b/src/strategies/base.rs @@ -838,11 +838,11 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// * `Err(PositionError)` - If there is an error retrieving the positions. fn get_total_cost(&self) -> Result { let positions = self.get_positions()?; - let costs = positions - .iter() - .map(|p| p.total_cost().unwrap()) - .sum::(); - Ok(costs) + let mut total = Positive::ZERO; + for p in positions { + total += p.total_cost()?; + } + Ok(total) } /// Calculates the net cost of the strategy, which is the sum of the costs of all positions, @@ -853,11 +853,11 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// * `Err(PositionError)` - If there is an error retrieving the positions. fn get_net_cost(&self) -> Result { let positions = self.get_positions()?; - let costs = positions - .iter() - .map(|p| p.net_cost().unwrap()) - .sum::(); - Ok(costs) + let mut total = Decimal::ZERO; + for p in positions { + total += p.net_cost()?; + } + Ok(total) } /// Calculates the net premium received for the strategy. This is the total premium received from short positions @@ -868,16 +868,15 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { /// * `Err(StrategyError)` - If there is an error retrieving the positions. fn get_net_premium_received(&self) -> Result { let positions = self.get_positions()?; - let costs = positions - .iter() - .filter(|p| p.option.side == Side::Long) - .map(|p| p.net_cost().unwrap()) - .sum::(); - let premiums = positions - .iter() - .filter(|p| p.option.side == Side::Short) - .map(|p| p.net_premium_received().unwrap()) - .sum::(); + let mut costs = Decimal::ZERO; + let mut premiums = Positive::ZERO; + for p in positions { + if p.option.side == Side::Long { + costs += p.net_cost()?; + } else if p.option.side == Side::Short { + premiums += p.net_premium_received()?; + } + } match premiums > costs { true => Ok(premiums - costs), false => Ok(Positive::ZERO), @@ -961,10 +960,17 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { all_points.push((*underlying_price + max_diff).max(last_strike)); // Sort to find min and max - all_points.sort_by(|a, b| a.partial_cmp(b).unwrap()); - - let start_price = *all_points.first().unwrap() * STRIKE_PRICE_LOWER_BOUND_MULTIPLIER; - let end_price = *all_points.last().unwrap() * STRIKE_PRICE_UPPER_BOUND_MULTIPLIER; + // SAFETY: total order on Positive; f64 fallback to Equal is safe for stable sort + all_points.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + + let first = all_points.first().ok_or_else(|| { + StrategyError::empty_collection("get_range_to_show: all_points is empty") + })?; + let last = all_points.last().ok_or_else(|| { + StrategyError::empty_collection("get_range_to_show: all_points is empty") + })?; + let start_price = *first * STRIKE_PRICE_LOWER_BOUND_MULTIPLIER; + let end_price = *last * STRIKE_PRICE_UPPER_BOUND_MULTIPLIER; Ok((start_price, end_price)) } @@ -1034,8 +1040,20 @@ pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble { 2 => Ok(break_even_points[1] - break_even_points[0]), _ => { // sort break even points and then get last minus first - break_even_points.sort_by(|a, b| a.partial_cmp(b).unwrap()); - Ok(*break_even_points.last().unwrap() - *break_even_points.first().unwrap()) + // SAFETY: total order on Positive; f64 fallback to Equal is safe for stable sort + break_even_points + .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + let last = break_even_points.last().ok_or_else(|| { + StrategyError::empty_collection( + "get_range_of_profit: break_even_points is empty", + ) + })?; + let first = break_even_points.first().ok_or_else(|| { + StrategyError::empty_collection( + "get_range_of_profit: break_even_points is empty", + ) + })?; + Ok(*last - *first) } } } @@ -1251,10 +1269,9 @@ pub trait Optimizable: Validable + Strategies { } FindOptimalSide::DeltaRange(min, max) => { let (delta_call, delta_put) = option.current_deltas(); - (delta_put.is_some() && delta_put.unwrap() >= *min && delta_put.unwrap() <= *max) - || (delta_call.is_some() - && delta_call.unwrap() >= *min - && delta_call.unwrap() <= *max) + let put_in = delta_put.is_some_and(|d| d >= *min && d <= *max); + let call_in = delta_call.is_some_and(|d| d >= *min && d <= *max); + put_in || call_in } } } @@ -1275,13 +1292,28 @@ pub trait Optimizable: Validable + Strategies { } /// Creates a new strategy from the given `OptionChain` and `StrategyLegs`. - /// The default implementation panics. Specific strategies must override this. + /// + /// Specific strategies must override this method. The default implementation + /// returns `StrategyError::OperationError(NotSupported { .. })`. /// /// # Arguments /// * `_chain` - A reference to the `OptionChain` providing option data. /// * `_legs` - A reference to the `StrategyLegs` defining the strategy's components. - fn create_strategy(&self, _chain: &OptionChain, _legs: &StrategyLegs) -> Self::Strategy { - unimplemented!("Create strategy is not applicable for this strategy"); + /// + /// # Errors + /// + /// Returns `StrategyError::OperationError` if the strategy cannot be built + /// from the supplied legs (e.g., missing bid/ask quotes, invalid leg + /// combination, or operation not supported by the concrete strategy). + fn create_strategy( + &self, + _chain: &OptionChain, + _legs: &StrategyLegs, + ) -> Result { + Err(StrategyError::operation_not_supported( + "create_strategy", + std::any::type_name::(), + )) } } diff --git a/src/strategies/bear_call_spread.rs b/src/strategies/bear_call_spread.rs index eaed98aa..76441fc0 100644 --- a/src/strategies/bear_call_spread.rs +++ b/src/strategies/bear_call_spread.rs @@ -127,10 +127,13 @@ impl BearCallSpread { /// /// # Panics /// - /// This function will panic if: - /// - Adding the short or long call positions fails - /// - Validating the strategy fails (e.g., if short strike price is >= long strike price) - /// - Calculating break-even points fails + /// # Errors + /// + /// Returns `StrategyError` if either freshly-constructed leg cannot be + /// added to the strategy or if the break-even calculation fails. In + /// practice these branches are unreachable for a freshly-built bear + /// call spread and are surfaced only to keep the constructor + /// panic-free. #[allow(clippy::too_many_arguments)] pub fn new( underlying_symbol: String, @@ -148,7 +151,7 @@ impl BearCallSpread { close_fee_short_call: Positive, open_fee_long_call: Positive, close_fee_long_call: Positive, - ) -> Self { + ) -> Result { if short_strike == Positive::ZERO { short_strike = underlying_price; } @@ -188,9 +191,7 @@ impl BearCallSpread { None, None, ); - strategy - .add_position(&short_call) - .expect("Error adding short call"); + strategy.add_position(&short_call)?; let long_call_option = Options::new( OptionType::European, @@ -215,16 +216,12 @@ impl BearCallSpread { None, None, ); - strategy - .add_position(&long_call) - .expect("Error adding long call"); + strategy.add_position(&long_call)?; strategy.validate(); - strategy - .update_break_even_points() - .expect("Unable to update break even points"); - strategy + strategy.update_break_even_points()?; + Ok(strategy) } } @@ -242,11 +239,12 @@ impl StrategyConstructor for BearCallSpread { // Sort options by strike price to identify short and long positions let mut sorted_positions = vec_positions.to_vec(); + // SAFETY: total order on Positive; f64 fallback to Equal is safe for stable sort sorted_positions.sort_by(|a, b| { a.option .strike_price .partial_cmp(&b.option.strike_price) - .unwrap() + .unwrap_or(std::cmp::Ordering::Equal) }); let lower_strike_position = &sorted_positions[0]; @@ -680,10 +678,10 @@ impl Optimizable for BearCallSpread { first: short_option, second: long_option, }; - let strategy = strategy.create_strategy(option_chain, &legs); - strategy.validate() - && strategy.get_max_profit().is_ok() - && strategy.get_max_loss().is_ok() + match strategy.create_strategy(option_chain, &legs) { + Ok(s) => s.validate() && s.get_max_profit().is_ok() && s.get_max_loss().is_ok(), + Err(_) => false, + } }) // Map to OptionDataGroup .map(move |(short, long)| OptionDataGroup::Two(short, long)) @@ -710,11 +708,24 @@ impl Optimizable for BearCallSpread { first: short_option, second: long_option, }; - let strategy = self.create_strategy(option_chain, &legs); + let strategy = match self.create_strategy(option_chain, &legs) { + Ok(s) => s, + Err(e) => { + tracing::warn!(error = %e, "skipping invalid strategy combination"); + continue; + } + }; // Calculate the current value based on the optimization criteria - let current_value = match criteria { - OptimizationCriteria::Ratio => strategy.get_profit_ratio().unwrap(), - OptimizationCriteria::Area => strategy.get_profit_area().unwrap(), + let metric = match criteria { + OptimizationCriteria::Ratio => strategy.get_profit_ratio(), + OptimizationCriteria::Area => strategy.get_profit_area(), + }; + let current_value = match metric { + Ok(v) => v, + Err(e) => { + tracing::warn!(error = %e, "skipping candidate with unscorable metric"); + continue; + } }; if current_value > best_value { @@ -726,13 +737,36 @@ impl Optimizable for BearCallSpread { } } - fn create_strategy(&self, chain: &OptionChain, legs: &StrategyLegs) -> Self::Strategy { + /// Constructs a `BearCallSpread` from the supplied chain and legs. + /// + /// # Errors + /// + /// Returns `StrategyError::OperationError` when the supplied legs are + /// missing required quotes (`short.call_bid`, `long.call_ask`) needed to + /// price the spread. + fn create_strategy( + &self, + chain: &OptionChain, + legs: &StrategyLegs, + ) -> Result { let (short, long) = match legs { StrategyLegs::TwoLegs { first, second } => (first, second), _ => panic!("Invalid number of legs for this strategy"), }; let implied_volatility = short.implied_volatility; assert!(implied_volatility <= Positive::ONE); + let short_call_bid = short.call_bid.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing call_bid for short leg", + ) + })?; + let long_call_ask = long.call_ask.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing call_ask for long leg", + ) + })?; BearCallSpread::new( chain.symbol.clone(), chain.underlying_price, @@ -743,8 +777,8 @@ impl Optimizable for BearCallSpread { self.short_call.option.risk_free_rate, self.short_call.option.dividend_yield, self.short_call.option.quantity, - short.call_bid.unwrap(), - long.call_ask.unwrap(), + short_call_bid, + long_call_ask, self.short_call.open_fee, self.short_call.close_fee, self.long_call.open_fee, @@ -902,6 +936,7 @@ mod tests_bear_call_spread_strategies { pos_or_panic!(0.5), // open_fee_long_call pos_or_panic!(0.5), // close_fee_long_call ) + .unwrap() } #[test] @@ -1072,7 +1107,8 @@ mod tests_bear_call_spread_strategies { pos_or_panic!(0.5), pos_or_panic!(0.5), pos_or_panic!(0.5), - ); + ) + .unwrap(); // Check that all calculations scale properly with quantity assert_relative_eq!( @@ -1105,7 +1141,8 @@ mod tests_bear_call_spread_strategies { pos_or_panic!(0.5), pos_or_panic!(0.5), pos_or_panic!(0.5), - ); + ) + .unwrap(); // Check that strike width affects max loss calculation let base_spread = create_test_spread(); @@ -1173,7 +1210,8 @@ mod tests_bear_call_spread_positionable { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); let short_position = create_test_position(Side::Short); let result = spread.add_position(&short_position); @@ -1200,7 +1238,8 @@ mod tests_bear_call_spread_positionable { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); let long_position = create_test_position(Side::Long); let result = spread.add_position(&long_position); @@ -1227,7 +1266,8 @@ mod tests_bear_call_spread_positionable { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); let result = spread.get_positions(); assert!(result.is_ok()); @@ -1256,7 +1296,8 @@ mod tests_bear_call_spread_positionable { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); let short_position = create_test_position(Side::Short); let long_position = create_test_position(Side::Long); @@ -1286,7 +1327,8 @@ mod tests_bear_call_spread_positionable { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); // Create new positions let new_short = create_test_position(Side::Short); @@ -1315,7 +1357,8 @@ mod tests_bear_call_spread_positionable { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); let short_position = create_test_position(Side::Short); let long_position = create_test_position(Side::Long); @@ -1362,6 +1405,7 @@ mod tests_bear_call_spread_validable { Positive::ZERO, Positive::ZERO, ) + .unwrap() } #[test] @@ -1388,7 +1432,8 @@ mod tests_bear_call_spread_validable { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); assert!(!spread.validate()); } @@ -1410,7 +1455,8 @@ mod tests_bear_call_spread_validable { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); assert!(!spread.validate()); } @@ -1449,7 +1495,8 @@ mod tests_bear_call_spread_validable { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); assert!(!spread.validate()); } @@ -1471,7 +1518,8 @@ mod tests_bear_call_spread_validable { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); assert!(!spread.validate()); } @@ -1493,7 +1541,8 @@ mod tests_bear_call_spread_validable { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); // Should still be valid as long as strikes are different assert!(spread.validate()); } @@ -1516,7 +1565,8 @@ mod tests_bear_call_spread_validable { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); // Should be valid as quantity > 0 assert!(spread.validate()); } @@ -1552,6 +1602,7 @@ mod tests_bear_call_spread_profit { Positive::ZERO, // open_fee_long_call Positive::ZERO, // close_fee_long_call ) + .unwrap() } #[test] @@ -1658,7 +1709,8 @@ mod tests_bear_call_spread_profit { Positive::ZERO, // close_fee_short_call Positive::ZERO, // open_fee_long_call Positive::ZERO, // close_fee_long_call - ); + ) + .unwrap(); let profit = spread .calculate_profit_at(&pos_or_panic!(90.0)) @@ -1697,7 +1749,8 @@ mod tests_bear_call_spread_profit { pos_or_panic!(0.78), // open_fee_long pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short - ); + ) + .unwrap(); let profit = spread .calculate_profit_at(&pos_or_panic!(90.0)) @@ -1800,6 +1853,7 @@ mod tests_bear_call_spread_optimizable { Positive::ZERO, Positive::ZERO, ) + .unwrap() } #[test] @@ -1874,7 +1928,7 @@ mod tests_bear_call_spread_optimizable { second: long_option, }; - let new_strategy = strategy.create_strategy(&chain, &legs); + let new_strategy = strategy.create_strategy(&chain, &legs).unwrap(); // Verify the new strategy assert!(new_strategy.validate()); @@ -1981,12 +2035,12 @@ mod tests_bear_call_spread_optimizable { } #[test] - #[should_panic] fn test_create_strategy_invalid_legs() { let strategy = create_test_strategy(); let chain = create_mock_option_chain(); - // Test with invalid leg configuration + // Test with invalid leg configuration: same option twice should + // either fail to construct or fail validation. let result = std::panic::catch_unwind(|| { strategy.create_strategy( &chain, @@ -1994,10 +2048,14 @@ mod tests_bear_call_spread_optimizable { first: chain.options.iter().next().unwrap(), second: chain.options.iter().next().unwrap(), }, - ); + ) }); - assert!(result.is_err()); + match result { + Err(_) => {} + Ok(Err(_)) => {} + Ok(Ok(s)) => assert!(!s.validate(), "duplicate legs should not validate"), + } } } @@ -2026,6 +2084,7 @@ mod tests_bear_call_spread_graph { Positive::ZERO, // open_fee_long_call Positive::ZERO, // close_fee_long_call ) + .unwrap() } #[test] @@ -2064,6 +2123,7 @@ mod tests_bear_call_spread_probability { pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short ) + .unwrap() } #[test] @@ -2214,6 +2274,7 @@ mod tests_delta { pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short ) + .unwrap() } #[test] @@ -2360,6 +2421,7 @@ mod tests_delta_size { pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short ) + .unwrap() } #[test] @@ -2504,6 +2566,7 @@ mod tests_bear_call_spread_position_management { pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short ) + .unwrap() } #[test] @@ -2616,6 +2679,7 @@ mod tests_adjust_option_position_short { pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short ) + .unwrap() } #[test] diff --git a/src/strategies/bear_put_spread.rs b/src/strategies/bear_put_spread.rs index 5db3c451..87653152 100644 --- a/src/strategies/bear_put_spread.rs +++ b/src/strategies/bear_put_spread.rs @@ -128,6 +128,14 @@ impl BearPutSpread { /// /// The maximum profit is limited to the difference between strike prices minus /// the net premium paid, while the maximum loss is limited to the net premium paid. + /// + /// # Errors + /// + /// Returns `StrategyError` if either freshly-constructed leg cannot be + /// added to the strategy or if the break-even calculation fails. In + /// practice these branches are unreachable for a freshly-built bear + /// put spread and are surfaced only to keep the constructor + /// panic-free. #[allow(clippy::too_many_arguments)] pub fn new( underlying_symbol: String, @@ -145,7 +153,7 @@ impl BearPutSpread { close_fee_long_put: Positive, open_fee_short_put: Positive, close_fee_short_put: Positive, - ) -> Self { + ) -> Result { if long_strike == Positive::ZERO { long_strike = underlying_price; } @@ -185,9 +193,7 @@ impl BearPutSpread { None, None, ); - strategy - .add_position(&long_put) - .expect("Error adding long put"); + strategy.add_position(&long_put)?; let short_put_option = Options::new( OptionType::European, @@ -212,16 +218,12 @@ impl BearPutSpread { None, None, ); - strategy - .add_position(&short_put) - .expect("Error adding short put"); + strategy.add_position(&short_put)?; strategy.validate(); - strategy - .update_break_even_points() - .expect("Unable to update break even points"); - strategy + strategy.update_break_even_points()?; + Ok(strategy) } } @@ -239,11 +241,12 @@ impl StrategyConstructor for BearPutSpread { // Sort options by strike price to identify short and long positions let mut sorted_positions = vec_positions.to_vec(); + // SAFETY: total order on Positive; f64 fallback to Equal is safe for stable sort sorted_positions.sort_by(|a, b| { a.option .strike_price .partial_cmp(&b.option.strike_price) - .unwrap() + .unwrap_or(std::cmp::Ordering::Equal) }); let lower_strike_position = &sorted_positions[0]; @@ -673,10 +676,10 @@ impl Optimizable for BearPutSpread { first: short, second: long, }; - let strategy = strategy.create_strategy(option_chain, &legs); - strategy.validate() - && strategy.get_max_profit().is_ok() - && strategy.get_max_loss().is_ok() + match strategy.create_strategy(option_chain, &legs) { + Ok(s) => s.validate() && s.get_max_profit().is_ok() && s.get_max_loss().is_ok(), + Err(_) => false, + } }) // Map to OptionDataGroup .map(move |(short, long)| OptionDataGroup::Two(short, long)) @@ -703,11 +706,24 @@ impl Optimizable for BearPutSpread { first: short, second: long, }; - let strategy = self.create_strategy(option_chain, &legs); + let strategy = match self.create_strategy(option_chain, &legs) { + Ok(s) => s, + Err(e) => { + tracing::warn!(error = %e, "skipping invalid strategy combination"); + continue; + } + }; // Calculate the current value based on the optimization criteria - let current_value = match criteria { - OptimizationCriteria::Ratio => strategy.get_profit_ratio().unwrap(), - OptimizationCriteria::Area => strategy.get_profit_area().unwrap(), + let metric = match criteria { + OptimizationCriteria::Ratio => strategy.get_profit_ratio(), + OptimizationCriteria::Area => strategy.get_profit_area(), + }; + let current_value = match metric { + Ok(v) => v, + Err(e) => { + tracing::warn!(error = %e, "skipping candidate with unscorable metric"); + continue; + } }; if current_value > best_value { @@ -719,13 +735,36 @@ impl Optimizable for BearPutSpread { } } - fn create_strategy(&self, chain: &OptionChain, legs: &StrategyLegs) -> Self::Strategy { + /// Constructs a `BearPutSpread` from the supplied chain and legs. + /// + /// # Errors + /// + /// Returns `StrategyError::OperationError` when the supplied legs are + /// missing required quotes (`long.put_ask`, `short.put_bid`) needed to + /// price the spread. + fn create_strategy( + &self, + chain: &OptionChain, + legs: &StrategyLegs, + ) -> Result { let (short, long) = match legs { StrategyLegs::TwoLegs { first, second } => (first, second), _ => panic!("Invalid number of legs for this strategy"), }; let implied_volatility = long.implied_volatility; assert!(implied_volatility <= Positive::ONE); + let long_put_ask = long.put_ask.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing put_ask for long leg", + ) + })?; + let short_put_bid = short.put_bid.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing put_bid for short leg", + ) + })?; BearPutSpread::new( chain.symbol.clone(), chain.underlying_price, @@ -736,8 +775,8 @@ impl Optimizable for BearPutSpread { self.long_put.option.risk_free_rate, self.long_put.option.dividend_yield, self.long_put.option.quantity, - long.put_ask.unwrap(), - short.put_bid.unwrap(), + long_put_ask, + short_put_bid, self.long_put.open_fee, self.long_put.close_fee, self.short_put.open_fee, @@ -899,6 +938,7 @@ mod tests_bear_put_spread_strategy { Positive::ZERO, // open_fee_short_put Positive::ZERO, // close_fee_short_put ) + .unwrap() } #[test] @@ -1003,7 +1043,8 @@ mod tests_bear_put_spread_strategy { pos_or_panic!(0.5), // close_fee_long_put pos_or_panic!(0.5), // open_fee_short_put pos_or_panic!(0.5), // close_fee_short_put - ); + ) + .unwrap(); assert_eq!(spread.get_fees().unwrap().to_f64(), 2.0); // Total fees = 0.5 * 4 } @@ -1052,7 +1093,8 @@ mod tests_bear_put_spread_strategy { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); assert_eq!(spread.long_put.option.strike_price, Positive::HUNDRED); assert_eq!(spread.short_put.option.strike_price, Positive::HUNDRED); @@ -1076,7 +1118,8 @@ mod tests_bear_put_spread_strategy { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); let max_profit = spread.get_max_profit().unwrap(); let max_loss = spread.get_max_loss().unwrap(); @@ -1411,6 +1454,7 @@ mod tests_bear_put_spread_optimization { Positive::ZERO, Positive::ZERO, ) + .unwrap() } #[test] @@ -1494,7 +1538,7 @@ mod tests_bear_put_spread_optimization { first: long_option, second: short_option, }; - let new_strategy = spread.create_strategy(&chain, &legs); + let new_strategy = spread.create_strategy(&chain, &legs).unwrap(); assert!(new_strategy.validate()); assert_eq!( @@ -1558,7 +1602,8 @@ mod tests_bear_put_spread_optimization { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); let chain = create_test_chain(); @@ -1657,6 +1702,7 @@ mod tests_bear_put_spread_optimizable { Positive::ZERO, Positive::ZERO, ) + .unwrap() } #[test] @@ -1858,6 +1904,7 @@ mod tests_bear_put_spread_profit { Positive::ZERO, // open_fee_short_put Positive::ZERO, // close_fee_short_put ) + .unwrap() } #[test] @@ -1973,7 +2020,8 @@ mod tests_bear_put_spread_profit { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); let max_profit_price = pos_or_panic!(90.0); let max_loss_price = pos_or_panic!(110.0); @@ -2010,7 +2058,8 @@ mod tests_bear_put_spread_profit { pos_or_panic!(0.5), // close_fee_long_put pos_or_panic!(0.5), // open_fee_short_put pos_or_panic!(0.5), // close_fee_short_put - ); + ) + .unwrap(); let max_profit_price = pos_or_panic!(90.0); @@ -2065,6 +2114,7 @@ mod tests_bear_put_spread_probability { Positive::ZERO, // open_fee_short_put Positive::ZERO, // close_fee_short_put ) + .unwrap() } #[test] @@ -2211,6 +2261,7 @@ mod tests_bear_put_spread_graph { Positive::ZERO, Positive::ZERO, ) + .unwrap() } #[test] @@ -2254,6 +2305,7 @@ mod tests_delta { pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short ) + .unwrap() } #[test] @@ -2399,6 +2451,7 @@ mod tests_delta_size { pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short ) + .unwrap() } #[test] @@ -2544,6 +2597,7 @@ mod tests_bear_call_spread_position_management { pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short ) + .unwrap() } #[test] @@ -2655,6 +2709,7 @@ mod tests_adjust_option_position { pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short ) + .unwrap() } #[test] diff --git a/src/strategies/bull_call_spread.rs b/src/strategies/bull_call_spread.rs index 08c68e7b..c3a95b23 100644 --- a/src/strategies/bull_call_spread.rs +++ b/src/strategies/bull_call_spread.rs @@ -127,12 +127,13 @@ impl BullCallSpread { /// /// Returns a fully configured `BullCallSpread` strategy instance with positions and break-even points calculated. /// - /// # Panics + /// # Errors /// - /// This function will panic if: - /// - The long call position cannot be added to the strategy - /// - The short call position cannot be added to the strategy - /// - Break-even points cannot be calculated + /// Returns `StrategyError` if either freshly-constructed leg cannot be + /// added to the strategy or if the break-even calculation fails. In + /// practice these branches are unreachable for a freshly-built bull + /// call spread and are surfaced only to keep the constructor + /// panic-free. #[allow(clippy::too_many_arguments)] pub fn new( underlying_symbol: String, @@ -150,7 +151,7 @@ impl BullCallSpread { close_fee_long_call: Positive, open_fee_short_call: Positive, close_fee_short_call: Positive, - ) -> Self { + ) -> Result { if long_strike == Positive::ZERO { long_strike = underlying_price; } @@ -190,9 +191,7 @@ impl BullCallSpread { None, None, ); - strategy - .add_position(&long_call) - .expect("Failed to add long call"); + strategy.add_position(&long_call)?; let short_call_option = Options::new( OptionType::European, @@ -217,16 +216,12 @@ impl BullCallSpread { None, None, ); - strategy - .add_position(&short_call) - .expect("Failed to add short call"); + strategy.add_position(&short_call)?; strategy.validate(); - strategy - .update_break_even_points() - .expect("Unable to update break even points"); - strategy + strategy.update_break_even_points()?; + Ok(strategy) } } @@ -244,11 +239,12 @@ impl StrategyConstructor for BullCallSpread { // Sort options by strike price to identify long and short positions let mut sorted_positions = vec_positions.to_vec(); + // SAFETY: total order on Positive; f64 fallback to Equal is safe for stable sort sorted_positions.sort_by(|a, b| { a.option .strike_price .partial_cmp(&b.option.strike_price) - .unwrap() + .unwrap_or(std::cmp::Ordering::Equal) }); let lower_strike_option = &sorted_positions[0]; @@ -686,10 +682,10 @@ impl Optimizable for BullCallSpread { first: long, second: short, }; - let strategy = strategy.create_strategy(option_chain, &legs); - strategy.validate() - && strategy.get_max_profit().is_ok() - && strategy.get_max_loss().is_ok() + match strategy.create_strategy(option_chain, &legs) { + Ok(s) => s.validate() && s.get_max_profit().is_ok() && s.get_max_loss().is_ok(), + Err(_) => false, + } }) // Map to OptionDataGroup .map(move |(long, short)| OptionDataGroup::Two(long, short)) @@ -716,11 +712,24 @@ impl Optimizable for BullCallSpread { first: long, second: short, }; - let strategy = self.create_strategy(option_chain, &legs); + let strategy = match self.create_strategy(option_chain, &legs) { + Ok(s) => s, + Err(e) => { + tracing::warn!(error = %e, "skipping invalid strategy combination"); + continue; + } + }; // Calculate the current value based on the optimization criteria - let current_value = match criteria { - OptimizationCriteria::Ratio => strategy.get_profit_ratio().unwrap(), - OptimizationCriteria::Area => strategy.get_profit_area().unwrap(), + let metric = match criteria { + OptimizationCriteria::Ratio => strategy.get_profit_ratio(), + OptimizationCriteria::Area => strategy.get_profit_area(), + }; + let current_value = match metric { + Ok(v) => v, + Err(e) => { + tracing::warn!(error = %e, "skipping candidate with unscorable metric"); + continue; + } }; if current_value > best_value { @@ -732,13 +741,36 @@ impl Optimizable for BullCallSpread { } } - fn create_strategy(&self, chain: &OptionChain, legs: &StrategyLegs) -> Self::Strategy { + /// Constructs a `BullCallSpread` from the supplied chain and legs. + /// + /// # Errors + /// + /// Returns `StrategyError::OperationError` when the supplied legs are + /// missing required quotes (`long.call_ask`, `short.call_bid`) needed to + /// price the spread. + fn create_strategy( + &self, + chain: &OptionChain, + legs: &StrategyLegs, + ) -> Result { let (long, short) = match legs { StrategyLegs::TwoLegs { first, second } => (first, second), _ => panic!("Invalid number of legs for this strategy"), }; let implied_volatility = long.implied_volatility; assert!(implied_volatility <= Positive::ONE); + let long_call_ask = long.call_ask.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing call_ask for long leg", + ) + })?; + let short_call_bid = short.call_bid.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing call_bid for short leg", + ) + })?; BullCallSpread::new( chain.symbol.clone(), chain.underlying_price, @@ -749,8 +781,8 @@ impl Optimizable for BullCallSpread { self.long_call.option.risk_free_rate, self.long_call.option.dividend_yield, self.long_call.option.quantity, - long.call_ask.unwrap(), - short.call_bid.unwrap(), + long_call_ask, + short_call_bid, self.long_call.open_fee, self.long_call.close_fee, self.short_call.open_fee, @@ -910,6 +942,7 @@ fn bull_call_spread_test() -> BullCallSpread { pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short ) + .unwrap() } #[cfg(test)] @@ -1016,7 +1049,8 @@ mod tests_bull_call_spread_strategy { pos_or_panic!(0.5), // close_fee_long_call pos_or_panic!(0.5), // open_fee_short_call pos_or_panic!(0.5), // close_fee_short_call - ); + ) + .unwrap(); assert_eq!(spread.get_fees().unwrap().to_f64(), 2.0); } @@ -1062,7 +1096,8 @@ mod tests_bull_call_spread_strategy { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); assert_eq!(spread.long_call.option.strike_price, Positive::HUNDRED); assert_eq!(spread.short_call.option.strike_price, Positive::HUNDRED); @@ -1086,7 +1121,8 @@ mod tests_bull_call_spread_strategy { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); assert!(!spread.validate()); } @@ -1419,6 +1455,7 @@ mod tests_bull_call_spread_optimization { Positive::ZERO, Positive::ZERO, ) + .unwrap() } #[test] @@ -1675,7 +1712,7 @@ mod tests_bull_call_spread_optimization { first: long_option, second: short_option, }; - let new_strategy = spread.create_strategy(&chain, &legs); + let new_strategy = spread.create_strategy(&chain, &legs).unwrap(); assert!(new_strategy.validate()); assert_eq!( @@ -1801,7 +1838,8 @@ mod tests_bull_call_spread_profit { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); let price = pos_or_panic!(105.0); assert_eq!( @@ -1832,7 +1870,8 @@ mod tests_bull_call_spread_profit { pos_or_panic!(0.5), // close_fee_long_call pos_or_panic!(0.5), // open_fee_short_call pos_or_panic!(0.5), // close_fee_short_call - ); + ) + .unwrap(); let price = pos_or_panic!(105.0); assert_eq!( @@ -2062,7 +2101,8 @@ mod tests_bull_call_spread_probability { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); let result = spread.probability_of_profit(None, None); assert!(result.is_ok()); @@ -2089,7 +2129,8 @@ mod tests_bull_call_spread_probability { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); let result = spread.probability_of_profit(None, None); assert!(result.is_ok()); @@ -2131,6 +2172,7 @@ mod tests_delta { pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short ) + .unwrap() } #[test] @@ -2276,6 +2318,7 @@ mod tests_delta_size { pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short ) + .unwrap() } #[test] @@ -2416,6 +2459,7 @@ mod tests_bull_call_spread_position_management { pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short ) + .unwrap() } #[test] @@ -2527,6 +2571,7 @@ mod tests_adjust_option_position { pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short ) + .unwrap() } #[test] diff --git a/src/strategies/bull_put_spread.rs b/src/strategies/bull_put_spread.rs index 036b3564..8d28342b 100644 --- a/src/strategies/bull_put_spread.rs +++ b/src/strategies/bull_put_spread.rs @@ -138,6 +138,14 @@ impl BullPutSpread { /// The created strategy is validated to ensure: /// 1. Both positions are valid /// 2. The long put strike price is lower than the short put strike price + /// + /// # Errors + /// + /// Returns `StrategyError` if either freshly-constructed leg cannot be + /// added to the strategy or if the break-even calculation fails. In + /// practice these branches are unreachable for a freshly-built bull + /// put spread and are surfaced only to keep the constructor + /// panic-free. #[allow(clippy::too_many_arguments)] pub fn new( underlying_symbol: String, @@ -155,7 +163,7 @@ impl BullPutSpread { close_fee_long_put: Positive, open_fee_short_put: Positive, close_fee_short_put: Positive, - ) -> Self { + ) -> Result { if long_strike == Positive::ZERO { long_strike = underlying_price; } @@ -195,9 +203,7 @@ impl BullPutSpread { None, None, ); - strategy - .add_position(&long_put) - .expect("Error adding long put"); + strategy.add_position(&long_put)?; let short_put_option = Options::new( OptionType::European, @@ -222,17 +228,13 @@ impl BullPutSpread { None, None, ); - strategy - .add_position(&short_put) - .expect("Error adding short put"); + strategy.add_position(&short_put)?; strategy.validate(); - strategy - .update_break_even_points() - .expect("Unable to update break even points"); + strategy.update_break_even_points()?; - strategy + Ok(strategy) } } @@ -250,11 +252,12 @@ impl StrategyConstructor for BullPutSpread { // Sort options by strike price to identify short and long positions let mut sorted_positions = vec_positions.to_vec(); + // SAFETY: total order on Positive; f64 fallback to Equal is safe for stable sort sorted_positions.sort_by(|a, b| { a.option .strike_price .partial_cmp(&b.option.strike_price) - .unwrap() + .unwrap_or(std::cmp::Ordering::Equal) }); let lower_strike_option = &sorted_positions[0]; @@ -727,7 +730,7 @@ impl Optimizable for BullPutSpread { /// pos_or_panic!(0.78), // open_fee_long /// pos_or_panic!(0.73), // close_fee_long /// pos_or_panic!(0.73), // close_fee_short - /// ); + /// ).expect("valid bull put spread"); /// /// let side = FindOptimalSide::Lower; /// let filtered_combinations = bull_put_spread_strategy.filter_combinations(&option_chain, side); @@ -783,10 +786,10 @@ impl Optimizable for BullPutSpread { first: long_option, second: short_option, }; - let strategy = strategy.create_strategy(option_chain, &legs); - strategy.validate() - && strategy.get_max_profit().is_ok() - && strategy.get_max_loss().is_ok() + match strategy.create_strategy(option_chain, &legs) { + Ok(s) => s.validate() && s.get_max_profit().is_ok() && s.get_max_loss().is_ok(), + Err(_) => false, + } }) // Map to OptionDataGroup .map(move |(long, short)| OptionDataGroup::Two(long, short)) @@ -813,11 +816,24 @@ impl Optimizable for BullPutSpread { first: long_option, second: short_option, }; - let strategy = self.create_strategy(option_chain, &legs); + let strategy = match self.create_strategy(option_chain, &legs) { + Ok(s) => s, + Err(e) => { + tracing::warn!(error = %e, "skipping invalid strategy combination"); + continue; + } + }; // Calculate the current value based on the optimization criteria - let current_value = match criteria { - OptimizationCriteria::Ratio => strategy.get_profit_ratio().unwrap(), - OptimizationCriteria::Area => strategy.get_profit_area().unwrap(), + let metric = match criteria { + OptimizationCriteria::Ratio => strategy.get_profit_ratio(), + OptimizationCriteria::Area => strategy.get_profit_area(), + }; + let current_value = match metric { + Ok(v) => v, + Err(e) => { + tracing::warn!(error = %e, "skipping candidate with unscorable metric"); + continue; + } }; if current_value > best_value { @@ -829,13 +845,36 @@ impl Optimizable for BullPutSpread { } } - fn create_strategy(&self, chain: &OptionChain, legs: &StrategyLegs) -> Self::Strategy { + /// Constructs a `BullPutSpread` from the supplied chain and legs. + /// + /// # Errors + /// + /// Returns `StrategyError::OperationError` when the supplied legs are + /// missing required quotes (`long.put_ask`, `short.put_bid`) needed to + /// price the spread. + fn create_strategy( + &self, + chain: &OptionChain, + legs: &StrategyLegs, + ) -> Result { let (long, short) = match legs { StrategyLegs::TwoLegs { first, second } => (first, second), _ => panic!("Invalid number of legs for this strategy"), }; let implied_volatility = long.implied_volatility; assert!(implied_volatility <= Positive::ONE); + let long_put_ask = long.put_ask.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing put_ask for long leg", + ) + })?; + let short_put_bid = short.put_bid.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing put_bid for short leg", + ) + })?; BullPutSpread::new( chain.symbol.clone(), chain.underlying_price, @@ -846,8 +885,8 @@ impl Optimizable for BullPutSpread { self.long_put.option.risk_free_rate, self.long_put.option.dividend_yield, self.long_put.option.quantity, - long.put_ask.unwrap(), - short.put_bid.unwrap(), + long_put_ask, + short_put_bid, self.long_put.open_fee, self.long_put.close_fee, self.short_put.open_fee, @@ -1004,6 +1043,7 @@ fn bull_put_spread_test() -> BullPutSpread { pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short ) + .unwrap() } #[cfg(test)] @@ -1147,7 +1187,8 @@ mod tests_bull_put_spread_strategy { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); assert_eq!(spread.long_put.option.strike_price, Positive::HUNDRED); assert_eq!(spread.short_put.option.strike_price, Positive::HUNDRED); @@ -1171,7 +1212,8 @@ mod tests_bull_put_spread_strategy { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); assert!(!spread.validate()); } @@ -1484,6 +1526,7 @@ mod tests_bull_put_spread_optimization { Positive::ZERO, Positive::ZERO, ) + .unwrap() } #[test] @@ -1686,7 +1729,7 @@ mod tests_bull_put_spread_optimization { first: long_option, second: short_option, }; - let new_strategy = spread.create_strategy(&chain, &legs); + let new_strategy = spread.create_strategy(&chain, &legs).unwrap(); assert!(new_strategy.validate()); assert_eq!( @@ -1806,7 +1849,8 @@ mod tests_bull_put_spread_profit { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); let price = pos_or_panic!(85.0); assert_eq!( @@ -1874,6 +1918,7 @@ mod tests_bull_put_spread_probability { Positive::ZERO, // open_fee_short_put Positive::ZERO, // close_fee_short_put ) + .unwrap() } #[test] @@ -2024,6 +2069,7 @@ mod tests_delta { pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short ) + .unwrap() } #[test] @@ -2167,6 +2213,7 @@ mod tests_delta_size { pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short ) + .unwrap() } #[test] @@ -2307,6 +2354,7 @@ mod tests_bear_call_spread_position_management { pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short ) + .unwrap() } #[test] @@ -2418,6 +2466,7 @@ mod tests_adjust_option_position { pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short ) + .unwrap() } #[test] diff --git a/src/strategies/call_butterfly.rs b/src/strategies/call_butterfly.rs index feef213f..86e86a2d 100644 --- a/src/strategies/call_butterfly.rs +++ b/src/strategies/call_butterfly.rs @@ -128,6 +128,13 @@ impl CallButterfly { /// # Returns /// /// A fully initialized `CallButterfly` strategy with all positions and break-even points calculated. + /// + /// # Errors + /// + /// Returns `StrategyError` if any freshly-constructed leg cannot be added + /// to the strategy or if the break-even calculation fails. In practice + /// these branches are unreachable for a freshly-built call butterfly and + /// are surfaced only to keep the constructor panic-free. #[allow(clippy::too_many_arguments)] pub fn new( underlying_symbol: String, @@ -149,7 +156,7 @@ impl CallButterfly { close_fee_short_low: Positive, open_fee_short_high: Positive, close_fee_short_high: Positive, - ) -> Self { + ) -> Result { let mut strategy = CallButterfly { name: underlying_symbol.to_string(), kind: StrategyType::CallButterfly, @@ -182,9 +189,7 @@ impl CallButterfly { None, None, ); - strategy - .add_position(&long_call) - .expect("Invalid short call"); + strategy.add_position(&long_call)?; strategy.long_call = long_call; let short_call_low_option = Options::new( @@ -210,9 +215,7 @@ impl CallButterfly { None, None, ); - strategy - .add_position(&short_call_low) - .expect("Invalid long call itm"); + strategy.add_position(&short_call_low)?; strategy.short_call_low = short_call_low; let short_call_high_option = Options::new( @@ -238,15 +241,11 @@ impl CallButterfly { None, None, ); - strategy - .add_position(&short_call_high) - .expect("Invalid long call otm"); + strategy.add_position(&short_call_high)?; strategy.short_call_high = short_call_high; - strategy - .update_break_even_points() - .expect("Unable to update break even points"); - strategy + strategy.update_break_even_points()?; + Ok(strategy) } } @@ -264,11 +263,12 @@ impl StrategyConstructor for CallButterfly { // Sort options by strike price let mut sorted_positions = vec_positions.to_vec(); + // SAFETY: total order on Positive; f64 fallback to Equal is safe for stable sort sorted_positions.sort_by(|a, b| { a.option .strike_price .partial_cmp(&b.option.strike_price) - .unwrap() + .unwrap_or(std::cmp::Ordering::Equal) }); let low_short_call_position = &sorted_positions[0]; @@ -725,7 +725,7 @@ impl Strategies for CallButterfly { } fn get_profit_ratio(&self) -> Result { - let max_loss = match self.get_max_loss().unwrap() { + let max_loss = match self.get_max_loss()? { value if value == Positive::ZERO => spos!(1.0), value if value == Positive::INFINITY => spos!(1.0), value => Some(value), @@ -804,10 +804,10 @@ impl Optimizable for CallButterfly { second: short_low, third: short_high, }; - let strategy = strategy.create_strategy(option_chain, &legs); - strategy.validate() - && strategy.get_max_profit().is_ok() - && strategy.get_max_loss().is_ok() + match strategy.create_strategy(option_chain, &legs) { + Ok(s) => s.validate() && s.get_max_profit().is_ok() && s.get_max_loss().is_ok(), + Err(_) => false, + } }) // Map to OptionDataGroup .map(move |(long, short_low, short_high)| { @@ -837,11 +837,24 @@ impl Optimizable for CallButterfly { second: short_low, third: short_high, }; - let strategy = self.create_strategy(option_chain, &legs); + let strategy = match self.create_strategy(option_chain, &legs) { + Ok(s) => s, + Err(e) => { + tracing::warn!(error = %e, "skipping invalid strategy combination"); + continue; + } + }; // Calculate the current value based on the optimization criteria - let current_value = match criteria { - OptimizationCriteria::Ratio => strategy.get_profit_ratio().unwrap(), - OptimizationCriteria::Area => strategy.get_profit_area().unwrap(), + let metric = match criteria { + OptimizationCriteria::Ratio => strategy.get_profit_ratio(), + OptimizationCriteria::Area => strategy.get_profit_area(), + }; + let current_value = match metric { + Ok(v) => v, + Err(e) => { + tracing::warn!(error = %e, "skipping candidate with unscorable metric"); + continue; + } }; if current_value > best_value { @@ -853,7 +866,19 @@ impl Optimizable for CallButterfly { } } - fn create_strategy(&self, option_chain: &OptionChain, legs: &StrategyLegs) -> CallButterfly { + /// Constructs a `CallButterfly` from the supplied chain and legs. + /// + /// # Errors + /// + /// Returns `StrategyError::OperationError` when the supplied legs are + /// missing required quotes (`long_call.call_ask`, + /// `short_call_low.call_bid`, `short_call_high.call_bid`) needed to + /// price the strategy. + fn create_strategy( + &self, + option_chain: &OptionChain, + legs: &StrategyLegs, + ) -> Result { let (long_call, short_call_low, short_call_high) = match legs { StrategyLegs::ThreeLegs { first, @@ -868,6 +893,24 @@ impl Optimizable for CallButterfly { } let implied_volatility = long_call.implied_volatility; assert!(implied_volatility <= Positive::ONE); + let long_call_ask = long_call.call_ask.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing call_ask for long call leg", + ) + })?; + let short_call_low_bid = short_call_low.call_bid.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing call_bid for short_call_low leg", + ) + })?; + let short_call_high_bid = short_call_high.call_bid.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing call_bid for short_call_high leg", + ) + })?; CallButterfly::new( option_chain.symbol.clone(), option_chain.underlying_price, @@ -879,9 +922,9 @@ impl Optimizable for CallButterfly { self.long_call.option.risk_free_rate, self.long_call.option.dividend_yield, self.long_call.option.quantity, - long_call.call_ask.unwrap(), - short_call_low.call_bid.unwrap(), - short_call_high.call_bid.unwrap(), + long_call_ask, + short_call_low_bid, + short_call_high_bid, self.long_call.open_fee, self.long_call.close_fee, self.short_call_low.open_fee, @@ -1078,6 +1121,7 @@ mod tests_call_butterfly { pos_or_panic!(0.1), pos_or_panic!(0.1), ) + .unwrap() } #[test] @@ -1161,6 +1205,7 @@ mod tests_call_butterfly_validation { pos_or_panic!(0.1), pos_or_panic!(0.1), ) + .unwrap() } #[test] @@ -1211,6 +1256,7 @@ mod tests_call_butterfly_delta { pos_or_panic!(0.73), // close_fee_short pos_or_panic!(0.73), // close_fee_short ) + .unwrap() } #[test] @@ -1382,6 +1428,7 @@ mod tests_call_butterfly_delta_size { pos_or_panic!(0.73), // close_fee_short pos_or_panic!(0.73), ) + .unwrap() } #[test] @@ -1608,6 +1655,7 @@ mod tests_call_butterfly_optimizable { pos_or_panic!(0.1), pos_or_panic!(0.1), ) + .unwrap() } #[test] @@ -1665,7 +1713,7 @@ mod tests_call_butterfly_optimizable { third: chain.options.iter().nth(2).unwrap(), }; - let new_strategy = butterfly.create_strategy(&chain, &legs); + let new_strategy = butterfly.create_strategy(&chain, &legs).unwrap(); // Verify the new strategy has correct properties assert_relative_eq!( @@ -1687,7 +1735,9 @@ mod tests_call_butterfly_optimizable { second: chain.options.iter().nth(1).unwrap(), }; - butterfly.create_strategy(&chain, &legs); // Should panic + // Wrong number of legs is still a panic (that branch is owned by + // issue #292, not panic-free core). + let _ = butterfly.create_strategy(&chain, &legs); } #[test] @@ -1744,6 +1794,7 @@ mod tests_call_butterfly_probability { pos_or_panic!(0.73), // close_fee_short pos_or_panic!(0.72), // open_fee_short ) + .unwrap() } #[test] @@ -1956,6 +2007,7 @@ mod tests_call_butterfly_position_management { pos_or_panic!(0.73), // close_fee_short pos_or_panic!(0.72), // open_fee_short ) + .unwrap() } #[test] @@ -2139,6 +2191,7 @@ mod tests_adjust_option_position { pos_or_panic!(0.73), // close_fee_short pos_or_panic!(0.72), // open_fee_short ) + .unwrap() } #[test] @@ -2447,6 +2500,7 @@ mod tests_call_butterfly_pnl { pos_or_panic!(0.1), pos_or_panic!(0.1), ) + .unwrap() } fn create_test_call_butterfly() -> Result { diff --git a/src/strategies/collar.rs b/src/strategies/collar.rs index 4555f02e..5d9693f2 100644 --- a/src/strategies/collar.rs +++ b/src/strategies/collar.rs @@ -177,11 +177,12 @@ impl Collar { /// /// A fully configured `Collar` strategy instance. /// - /// # Panics + /// # Errors /// - /// Panics if break-even point calculation fails. + /// Returns `StrategyError` if the break-even calculation fails. In + /// practice this branch is unreachable for a freshly-built collar and + /// is surfaced only to keep the constructor panic-free. #[allow(clippy::too_many_arguments)] - #[must_use] pub fn new( underlying_symbol: String, underlying_price: Positive, @@ -200,7 +201,7 @@ impl Collar { put_close_fee: Positive, call_open_fee: Positive, call_close_fee: Positive, - ) -> Self { + ) -> Result { // Create the spot position (long underlying) let spot_leg = SpotPosition::new( underlying_symbol.clone(), @@ -275,11 +276,9 @@ impl Collar { }; strategy.validate(); - strategy - .update_break_even_points() - .expect("Failed to calculate break-even points"); + strategy.update_break_even_points()?; - strategy + Ok(strategy) } /// Returns the spot leg as a `Leg` enum. @@ -860,6 +859,7 @@ mod tests { pos_or_panic!(0.65), // call open fee pos_or_panic!(0.65), // call close fee ) + .unwrap() } #[test] diff --git a/src/strategies/covered_call.rs b/src/strategies/covered_call.rs index 4fc9fb28..a0fcdbf7 100644 --- a/src/strategies/covered_call.rs +++ b/src/strategies/covered_call.rs @@ -149,8 +149,13 @@ impl CoveredCall { /// # Returns /// /// A fully configured `CoveredCall` strategy instance. + /// + /// # Errors + /// + /// Returns `StrategyError` if the break-even calculation fails. In + /// practice this branch is unreachable for a freshly-built covered + /// call and is surfaced only to keep the constructor panic-free. #[allow(clippy::too_many_arguments)] - #[must_use] pub fn new( underlying_symbol: String, underlying_price: Positive, @@ -165,7 +170,7 @@ impl CoveredCall { spot_close_fee: Positive, call_open_fee: Positive, call_close_fee: Positive, - ) -> Self { + ) -> Result { // Create the spot position (long underlying) let spot_leg = SpotPosition::new( underlying_symbol.clone(), @@ -213,11 +218,9 @@ impl CoveredCall { }; strategy.validate(); - strategy - .update_break_even_points() - .expect("Failed to calculate break-even points"); + strategy.update_break_even_points()?; - strategy + Ok(strategy) } /// Returns the spot leg as a `Leg` enum. @@ -690,6 +693,7 @@ mod tests { pos_or_panic!(0.65), pos_or_panic!(0.65), ) + .unwrap() } #[test] diff --git a/src/strategies/custom.rs b/src/strategies/custom.rs index 349e0fff..9fe8d13e 100644 --- a/src/strategies/custom.rs +++ b/src/strategies/custom.rs @@ -108,9 +108,12 @@ impl CustomStrategy { /// maximum profit, and maximum loss information. /// /// # Panics - /// Panics if the strategy validation fails or if break-even points cannot be calculated. - /// This typically occurs when the strategy has no positions or when the maximum loss point - /// cannot be determined. + /// Panics if the strategy validation fails (no positions). This is owned + /// by issue #292 and will be lifted to a `Result` variant separately. + /// + /// # Errors + /// + /// Returns `StrategyError` if the break-even calculation fails. #[allow(clippy::too_many_arguments)] pub fn new( name: String, @@ -121,7 +124,7 @@ impl CustomStrategy { epsilon: Positive, max_iterations: u32, step_by: Positive, - ) -> Self { + ) -> Result { let mut strategy = CustomStrategy { name, symbol, @@ -140,10 +143,8 @@ impl CustomStrategy { if strategy.positions.is_empty() { panic!("Invalid strategy: No positions provided"); } - strategy - .update_break_even_points() - .expect("Unable to update break even points"); - strategy + strategy.update_break_even_points()?; + Ok(strategy) } fn update_positions(&mut self, new_positions: Vec) { @@ -209,14 +210,17 @@ impl CustomStrategy { Ok(prices) } - /// Refine a break-even point guess using Newton-Raphson method + /// Refine a break-even point guess using Newton-Raphson method. + /// + /// Returns `None` instead of panicking if any per-iteration profit + /// evaluation or `Decimal -> f64` conversion fails. #[allow(dead_code)] fn refine_break_even_point(&self, initial_guess: Positive) -> Option { let mut x = initial_guess; let mut iterations = 0; while iterations < self.max_iterations { - let f_x = self.calculate_profit_at(&x).unwrap().to_f64().unwrap(); + let f_x = self.calculate_profit_at(&x).ok()?.to_f64()?; // Check if we're close enough to zero if f_x.abs() < self.epsilon { @@ -224,15 +228,9 @@ impl CustomStrategy { } // Calculate derivative numerically with smaller step - let f_x = self.calculate_profit_at(&x).unwrap().to_f64().unwrap(); let h = self.epsilon.sqrt(); - let derivative = (self - .calculate_profit_at(&(x + h)) - .unwrap() - .to_f64() - .unwrap() - - f_x) - / h; + let f_x_h = self.calculate_profit_at(&(x + h)).ok()?.to_f64()?; + let derivative = (f_x_h - f_x) / h; // Avoid division by very small numbers if derivative.abs() < self.epsilon { @@ -342,7 +340,7 @@ impl CustomStrategy { impl StrategyConstructor for CustomStrategy { fn get_strategy(vec_options: &[Position]) -> Result { - Ok(Self::new( + Self::new( "CustomStrategy".to_string(), "".to_string(), format!("CustomStrategy: {:?}", vec_options), @@ -351,7 +349,7 @@ impl StrategyConstructor for CustomStrategy { Default::default(), 100, Default::default(), - )) + ) } } @@ -444,7 +442,11 @@ impl Positionable for CustomStrategy { "Position not found: {:?} {:?}", option_style, side ))), - 1 => Ok(matching_positions.into_iter().next().unwrap()), + 1 => matching_positions.into_iter().next().ok_or_else(|| { + PositionError::invalid_position( + "matching_positions length is 1 but iterator yielded none", + ) + }), _ => Err(PositionError::invalid_position(&format!( "Multiple positions found: {:?} {:?}", option_style, side @@ -802,9 +804,16 @@ impl Optimizable for CustomStrategy { // Evaluate the current combination self.update_positions(current_positions.clone()); - let current_value = match criteria { - OptimizationCriteria::Ratio => self.get_profit_ratio().unwrap(), - OptimizationCriteria::Area => self.get_profit_area().unwrap(), + let metric = match criteria { + OptimizationCriteria::Ratio => self.get_profit_ratio(), + OptimizationCriteria::Area => self.get_profit_area(), + }; + let current_value = match metric { + Ok(v) => v, + Err(e) => { + tracing::warn!(error = %e, "skipping candidate with unscorable metric"); + return best_positions.clone(); + } }; if current_value > best_value { @@ -814,8 +823,10 @@ impl Optimizable for CustomStrategy { } best_positions.clone() - }) - .unwrap(); + }); + if let Err(e) = _result { + tracing::warn!(error = ?e, "process_n_times_iter failed during find_optimal"); + } if best_value == Decimal::MIN { error!("No valid combinations found"); @@ -860,20 +871,18 @@ impl ProbabilityAnalysis for CustomStrategy { .first() .map(|position| position.option.risk_free_rate); - profit_ranges.iter_mut().for_each(|range| { - range - .calculate_probability( - &self.underlying_price, - Some(VolatilityAdjustment { - base_volatility: mean_volatility, - std_dev_adjustment: std_dev, - }), - None, // PriceTrend - &expiration, - risk_free_rate, - ) - .unwrap(); - }); + for range in profit_ranges.iter_mut() { + range.calculate_probability( + &self.underlying_price, + Some(VolatilityAdjustment { + base_volatility: mean_volatility, + std_dev_adjustment: std_dev, + }), + None, // PriceTrend + &expiration, + risk_free_rate, + )?; + } Ok(profit_ranges) } @@ -899,20 +908,18 @@ impl ProbabilityAnalysis for CustomStrategy { .first() .map(|position| position.option.risk_free_rate); - loss_ranges.iter_mut().for_each(|range| { - range - .calculate_probability( - &self.underlying_price, - Some(VolatilityAdjustment { - base_volatility: mean_volatility, - std_dev_adjustment: std_dev, - }), - None, // PriceTrend - &expiration, - risk_free_rate, - ) - .unwrap(); - }); + for range in loss_ranges.iter_mut() { + range.calculate_probability( + &self.underlying_price, + Some(VolatilityAdjustment { + base_volatility: mean_volatility, + std_dev_adjustment: std_dev, + }), + None, // PriceTrend + &expiration, + risk_free_rate, + )?; + } Ok(loss_ranges) } @@ -937,30 +944,23 @@ impl PnLCalculator for CustomStrategy { expiration_date: ExpirationDate, implied_volatility: &Positive, ) -> Result { - Ok(self - .positions - .iter() - .map(|position| { - position - .calculate_pnl(market_price, expiration_date, implied_volatility) - .unwrap() - }) - .sum()) + let mut total = PnL::default(); + for position in &self.positions { + total = total + + position.calculate_pnl(market_price, expiration_date, implied_volatility)?; + } + Ok(total) } fn calculate_pnl_at_expiration( &self, underlying_price: &Positive, ) -> Result { - Ok(self - .positions - .iter() - .map(|position| { - position - .calculate_pnl_at_expiration(underlying_price) - .unwrap() - }) - .sum()) + let mut total = PnL::default(); + for position in &self.positions { + total = total + position.calculate_pnl_at_expiration(underlying_price)?; + } + Ok(total) } fn adjustments_pnl(&self, adjustment: &DeltaAdjustment) -> Result { diff --git a/src/strategies/delta_neutral/model.rs b/src/strategies/delta_neutral/model.rs index 5931589a..74c6e6e6 100644 --- a/src/strategies/delta_neutral/model.rs +++ b/src/strategies/delta_neutral/model.rs @@ -354,17 +354,18 @@ pub trait DeltaNeutrality: Greeks + Positionable + Strategies { return Err(GreeksError::StdError("No options found".to_string())); } let underlying_price = *self.get_underlying_price(); - let individual_deltas: Vec = options - .iter() - .map(|option| DeltaPositionInfo { - delta: option.delta().unwrap(), - delta_per_contract: option.delta().unwrap() / option.quantity, + let mut individual_deltas: Vec = Vec::with_capacity(options.len()); + for option in options.iter() { + let delta = option.delta()?; + individual_deltas.push(DeltaPositionInfo { + delta, + delta_per_contract: delta / option.quantity, quantity: option.quantity, strike: option.strike_price, option_style: option.option_style, side: option.side, - }) - .collect(); + }); + } Ok(DeltaInfo { net_delta: self.delta()?, @@ -1496,7 +1497,8 @@ mod tests_serialization { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + ) + .unwrap(); let delta_info = strategy.delta_neutrality().unwrap(); let adjustments = strategy.delta_adjustments().unwrap(); let response = DeltaNeutralResponse { diff --git a/src/strategies/delta_neutral/optimizer.rs b/src/strategies/delta_neutral/optimizer.rs index 69107a56..3ff32ad2 100644 --- a/src/strategies/delta_neutral/optimizer.rs +++ b/src/strategies/delta_neutral/optimizer.rs @@ -145,8 +145,10 @@ impl<'a> AdjustmentOptimizer<'a> { && let Ok(plan) = self.optimize_with_new_legs(delta_gap, gamma_gap) { trace!("New legs plan quality: {:.4}", plan.quality_score); - if best_plan.is_none() || plan.quality_score < best_plan.as_ref().unwrap().quality_score - { + let beats_best = best_plan + .as_ref() + .is_none_or(|cur| plan.quality_score < cur.quality_score); + if beats_best { best_plan = Some(plan); } } @@ -157,8 +159,10 @@ impl<'a> AdjustmentOptimizer<'a> { && let Ok(plan) = self.optimize_with_underlying(delta_gap) { trace!("Underlying plan quality: {:.4}", plan.quality_score); - if best_plan.is_none() || plan.quality_score < best_plan.as_ref().unwrap().quality_score - { + let beats_best = best_plan + .as_ref() + .is_none_or(|cur| plan.quality_score < cur.quality_score); + if beats_best { best_plan = Some(plan); } } @@ -193,7 +197,12 @@ impl<'a> AdjustmentOptimizer<'a> { }) .collect(); - legs_with_delta.sort_by(|a, b| b.2.abs().partial_cmp(&a.2.abs()).unwrap()); + // SAFETY: total order on Decimal; f64 fallback to Equal is safe for stable sort + legs_with_delta.sort_by(|a, b| { + b.2.abs() + .partial_cmp(&a.2.abs()) + .unwrap_or(std::cmp::Ordering::Equal) + }); // Greedily adjust quantities for (idx, _leg_delta, delta_per_contract) in legs_with_delta { @@ -338,7 +347,10 @@ impl<'a> AdjustmentOptimizer<'a> { .calculate_price_black_scholes() .unwrap_or(dec!(1)) .max(dec!(0.01)); - eff_b.partial_cmp(&eff_a).unwrap() + // SAFETY: total order on Decimal; f64 fallback to Equal is safe for stable sort + eff_b + .partial_cmp(&eff_a) + .unwrap_or(std::cmp::Ordering::Equal) }); Ok(candidates) @@ -456,10 +468,8 @@ impl<'a> AdjustmentOptimizer<'a> { None, )); } - AdjustmentAction::CloseLeg { leg_index } => { - if *leg_index < positions.len() { - positions.remove(*leg_index); - } + AdjustmentAction::CloseLeg { leg_index } if *leg_index < positions.len() => { + positions.remove(*leg_index); } _ => {} } diff --git a/src/strategies/graph.rs b/src/strategies/graph.rs index be6c6664..63612285 100644 --- a/src/strategies/graph.rs +++ b/src/strategies/graph.rs @@ -168,9 +168,9 @@ macro_rules! impl_graph_for_payoff_strategy { }; // Get min and max values for reference lines - if !range.is_empty() { - let min_price = range.first().unwrap().to_dec(); - let max_price = range.last().unwrap().to_dec(); + if let (Some(first), Some(last)) = (range.first(), range.last()) { + let min_price = first.to_dec(); + let max_price = last.to_dec(); // Add zero line points (horizontal line at y=0) zero_line.x.push(min_price); @@ -241,9 +241,16 @@ macro_rules! impl_graph_for_payoff_strategy { // Calculate profit at each price point and add to the series for price in range { - let profit = self - .calculate_profit_at(&price) - .unwrap(); + let profit = match self.calculate_profit_at(&price) { + Ok(p) => p, + Err(e) => { + ::tracing::warn!( + error = %e, + "skipping price point with unscorable profit" + ); + continue; + } + }; profit_series.x.push(price.to_dec()); profit_series.y.push(profit); @@ -266,10 +273,16 @@ macro_rules! impl_graph_for_payoff_strategy { }; // If the sign changes or it's the first point - if current_sign.is_none() || (sign != 0 && current_sign.unwrap() != sign) { + let sign_changed = match current_sign { + None => true, + Some(cur) => sign != 0 && cur != sign, + }; + if sign_changed { // If there are already points in the current segment, save it if !current_segment.is_empty() { - segments.push((current_segment, current_sign.unwrap())); + if let Some(cur) = current_sign { + segments.push((current_segment, cur)); + } current_segment = Vec::new(); } current_sign = Some(sign); @@ -280,8 +293,8 @@ macro_rules! impl_graph_for_payoff_strategy { } // Add the last segment if it's not empty - if !current_segment.is_empty() && current_sign.is_some() { - segments.push((current_segment, current_sign.unwrap())); + if let (false, Some(cur)) = (current_segment.is_empty(), current_sign) { + segments.push((current_segment, cur)); } // Create series for each segment diff --git a/src/strategies/iron_butterfly.rs b/src/strategies/iron_butterfly.rs index d5f359a2..c82efade 100644 --- a/src/strategies/iron_butterfly.rs +++ b/src/strategies/iron_butterfly.rs @@ -168,6 +168,12 @@ impl IronButterfly { /// /// - **Ideal Market Outlook**: Neutral, expecting low volatility with the underlying price remaining near the short strike price. /// + /// # Errors + /// + /// Returns `StrategyError` if any freshly-constructed leg cannot be added + /// to the strategy or if the break-even calculation fails. In practice + /// these branches are unreachable for a freshly-built iron butterfly and + /// are surfaced only to keep the constructor panic-free. #[allow(clippy::too_many_arguments)] pub fn new( underlying_symbol: String, @@ -186,7 +192,7 @@ impl IronButterfly { premium_long_put: Positive, open_fee: Positive, close_fee: Positive, - ) -> Self { + ) -> Result { let mut strategy = IronButterfly { name: "Iron Butterfly".to_string(), kind: StrategyType::IronButterfly, @@ -222,9 +228,7 @@ impl IronButterfly { None, None, ); - strategy - .add_position(&short_call) - .expect("Invalid short call"); + strategy.add_position(&short_call)?; // Short Put let short_put_option = Options::new( @@ -250,9 +254,7 @@ impl IronButterfly { None, None, ); - strategy - .add_position(&short_put) - .expect("Invalid short put"); + strategy.add_position(&short_put)?; // Long Call let long_call_option = Options::new( @@ -278,9 +280,7 @@ impl IronButterfly { None, None, ); - strategy - .add_position(&long_call) - .expect("Invalid long call"); + strategy.add_position(&long_call)?; // Long Put let long_put_option = Options::new( @@ -306,12 +306,10 @@ impl IronButterfly { None, None, ); - strategy.add_position(&long_put).expect("Invalid long put"); + strategy.add_position(&long_put)?; - strategy - .update_break_even_points() - .expect("Unable to update break even points"); - strategy + strategy.update_break_even_points()?; + Ok(strategy) } } @@ -329,11 +327,12 @@ impl StrategyConstructor for IronButterfly { // Sort options by strike price to identify positions let mut sorted_positions = vec_positions.to_vec(); + // SAFETY: total order on Positive; f64 fallback to Equal is safe for stable sort sorted_positions.sort_by(|a, b| { a.option .strike_price .partial_cmp(&b.option.strike_price) - .unwrap() + .unwrap_or(std::cmp::Ordering::Equal) }); // Validate the positions and their structure @@ -819,7 +818,7 @@ impl Strategies for IronButterfly { let result = (inner_area + outer_triangles) / self.short_call.option.underlying_price.to_f64(); - Ok(Decimal::from_f64(result).unwrap()) + Decimal::from_f64(result).ok_or_else(|| StrategyError::numeric_conversion(result)) } fn get_profit_ratio(&self) -> Result { @@ -881,10 +880,10 @@ impl Optimizable for IronButterfly { third: mid, fourth: high, }; - let strategy = strategy.create_strategy(option_chain, &legs); - strategy.validate() - && strategy.get_max_profit().is_ok() - && strategy.get_max_loss().is_ok() + match strategy.create_strategy(option_chain, &legs) { + Ok(s) => s.validate() && s.get_max_profit().is_ok() && s.get_max_loss().is_ok(), + Err(_) => false, + } }) // Map to OptionDataGroup .map(move |(low, mid, high)| OptionDataGroup::Three(low, mid, high)) @@ -913,11 +912,24 @@ impl Optimizable for IronButterfly { third: mid, fourth: high, }; - let strategy = self.create_strategy(option_chain, &legs); + let strategy = match self.create_strategy(option_chain, &legs) { + Ok(s) => s, + Err(e) => { + tracing::warn!(error = %e, "skipping invalid strategy combination"); + continue; + } + }; // Calculate the current value based on the optimization criteria - let current_value = match criteria { - OptimizationCriteria::Ratio => strategy.get_profit_ratio().unwrap(), - OptimizationCriteria::Area => strategy.get_profit_area().unwrap(), + let metric = match criteria { + OptimizationCriteria::Ratio => strategy.get_profit_ratio(), + OptimizationCriteria::Area => strategy.get_profit_area(), + }; + let current_value = match metric { + Ok(v) => v, + Err(e) => { + tracing::warn!(error = %e, "skipping candidate with unscorable metric"); + continue; + } }; if current_value > best_value { @@ -929,7 +941,19 @@ impl Optimizable for IronButterfly { } } - fn create_strategy(&self, chain: &OptionChain, legs: &StrategyLegs) -> Self::Strategy { + /// Constructs an `IronButterfly` from the supplied chain and legs. + /// + /// # Errors + /// + /// Returns `StrategyError::OperationError` when the supplied legs are + /// missing required quotes (`short_strike.call_bid`, + /// `short_strike.put_bid`, `long_call.call_ask`, `long_put.put_ask`) + /// needed to price the strategy. + fn create_strategy( + &self, + chain: &OptionChain, + legs: &StrategyLegs, + ) -> Result { match legs { StrategyLegs::FourLegs { first: long_put, @@ -939,6 +963,31 @@ impl Optimizable for IronButterfly { } => { let implied_volatility = short_strike.implied_volatility; assert!(implied_volatility <= Positive::ONE); + let short_call_bid = short_strike.call_bid.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing call_bid for short strike leg", + ) + })?; + let short_put_bid = short_strike.put_bid.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing put_bid for short strike leg", + ) + })?; + let long_call_ask = long_call.call_ask.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing call_ask for long call leg", + ) + })?; + let long_put_ask = long_put.put_ask.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing put_ask for long put leg", + ) + })?; + let fee_per_leg = self.get_fees()? / 8.0; IronButterfly::new( chain.symbol.clone(), chain.underlying_price, @@ -950,12 +999,12 @@ impl Optimizable for IronButterfly { self.short_call.option.risk_free_rate, self.short_call.option.dividend_yield, self.short_call.option.quantity, - short_strike.call_bid.unwrap(), - short_strike.put_bid.unwrap(), - long_call.call_ask.unwrap(), - long_put.put_ask.unwrap(), - self.get_fees().unwrap() / 8.0, - self.get_fees().unwrap() / 8.0, + short_call_bid, + short_put_bid, + long_call_ask, + long_put_ask, + fee_per_leg, + fee_per_leg, ) } _ => panic!("Invalid number of legs for Iron Butterfly strategy"), @@ -1157,7 +1206,8 @@ mod tests_iron_butterfly { Positive::ONE, // premium long put pos_or_panic!(5.0), // open fee pos_or_panic!(5.0), // close fee - ); + ) + .unwrap(); assert_eq!(butterfly.name, "Iron Butterfly"); assert_eq!( @@ -1192,7 +1242,8 @@ mod tests_iron_butterfly { Positive::ONE, pos_or_panic!(5.0), pos_or_panic!(5.0), - ); + ) + .unwrap(); assert_eq!(butterfly.get_max_loss().unwrap(), 49.0); } @@ -1217,7 +1268,8 @@ mod tests_iron_butterfly { Positive::TWO, pos_or_panic!(0.07), pos_or_panic!(0.07), - ); + ) + .unwrap(); let expected_profit: Positive = butterfly.get_net_premium_received().unwrap(); assert_eq!(butterfly.get_max_profit().unwrap(), expected_profit); @@ -1243,7 +1295,8 @@ mod tests_iron_butterfly { Positive::ONE, pos_or_panic!(5.0), pos_or_panic!(5.0), - ); + ) + .unwrap(); assert_eq!( butterfly.get_break_even_points().unwrap()[0], @@ -1281,7 +1334,8 @@ mod tests_iron_butterfly { Positive::ONE, pos_or_panic!(5.0), pos_or_panic!(5.0), - ); + ) + .unwrap(); let expected_fees = butterfly.short_call.open_fee + butterfly.short_call.close_fee @@ -1314,7 +1368,8 @@ mod tests_iron_butterfly { Positive::ONE, pos_or_panic!(5.0), pos_or_panic!(5.0), - ); + ) + .unwrap(); // Test at short strike (maximum profit point) let price = butterfly.short_call.option.strike_price; @@ -1396,6 +1451,7 @@ mod tests_iron_butterfly_validable { Positive::ZERO, // open_fee Positive::ZERO, // closing fee ) + .unwrap() } #[test] @@ -1555,6 +1611,7 @@ mod tests_iron_butterfly_strategies { pos_or_panic!(0.5), // open_fee pos_or_panic!(0.5), // closing fee ) + .unwrap() } #[test] @@ -1723,7 +1780,8 @@ mod tests_iron_butterfly_strategies { Positive::ONE, pos_or_panic!(0.5), pos_or_panic!(0.5), - ); + ) + .unwrap(); assert_eq!(butterfly.get_net_premium_received().unwrap().to_f64(), 0.0); } @@ -1747,7 +1805,8 @@ mod tests_iron_butterfly_strategies { Positive::ONE, pos_or_panic!(0.5), pos_or_panic!(0.5), - ); + ) + .unwrap(); assert_eq!(butterfly.get_net_premium_received().unwrap().to_f64(), 0.0); } @@ -1782,6 +1841,7 @@ mod tests_iron_butterfly_optimizable { pos_or_panic!(0.5), // open_fee pos_or_panic!(0.5), // closing fee ) + .unwrap() } fn create_test_chain() -> OptionChain { @@ -1945,7 +2005,7 @@ mod tests_iron_butterfly_optimizable { fourth: options[5], // 110.0 strike for long call }; - let new_strategy = butterfly.create_strategy(&chain, &legs); + let new_strategy = butterfly.create_strategy(&chain, &legs).unwrap(); assert!(new_strategy.validate()); assert_eq!( new_strategy.long_put.option.strike_price, @@ -1977,6 +2037,8 @@ mod tests_iron_butterfly_optimizable { second: options[1], }; + // Wrong number of legs is still a panic (that branch is owned by + // issue #292, not panic-free core). let _ = butterfly.create_strategy(&chain, &legs); } } @@ -2010,6 +2072,7 @@ mod tests_iron_butterfly_profit { Positive::ZERO, // open_fee Positive::ZERO, // closing fee ) + .unwrap() } #[test] @@ -2129,7 +2192,8 @@ mod tests_iron_butterfly_profit { Positive::ONE, pos_or_panic!(0.5), // open_fee pos_or_panic!(0.5), // closing fee - ); + ) + .unwrap(); let profit = butterfly .calculate_profit_at(&Positive::HUNDRED) @@ -2159,7 +2223,8 @@ mod tests_iron_butterfly_profit { Positive::ONE, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); let profit = butterfly .calculate_profit_at(&butterfly.short_call.option.strike_price) @@ -2241,6 +2306,7 @@ mod tests_iron_butterfly_delta { pos_or_panic!(0.96), // open_fee pos_or_panic!(0.96), // close_fee ) + .unwrap() } #[test] @@ -2434,6 +2500,7 @@ mod tests_iron_butterfly_delta_size { pos_or_panic!(0.96), // open_fee pos_or_panic!(0.96), // close_fee ) + .unwrap() } #[test] @@ -2626,6 +2693,7 @@ mod tests_iron_butterfly_probability { pos_or_panic!(0.96), // open_fee pos_or_panic!(0.96), // close_fee ) + .unwrap() } #[test] @@ -2831,6 +2899,7 @@ mod tests_iron_butterfly_position_management { pos_or_panic!(0.96), // open_fee pos_or_panic!(0.96), // close_fee ) + .unwrap() } #[test] @@ -3025,6 +3094,7 @@ mod tests_adjust_option_position { pos_or_panic!(0.96), // open_fee pos_or_panic!(0.96), // close_fee ) + .unwrap() } #[test] diff --git a/src/strategies/iron_condor.rs b/src/strategies/iron_condor.rs index 99aba58d..7421a53e 100644 --- a/src/strategies/iron_condor.rs +++ b/src/strategies/iron_condor.rs @@ -173,6 +173,12 @@ impl IronCondor { /// /// The short options generate income, while the long options limit the potential loss. /// + /// # Errors + /// + /// Returns `StrategyError` if any freshly-constructed leg cannot be added + /// to the strategy or if the break-even calculation fails. In practice + /// these branches are unreachable for a freshly-built iron condor and + /// are surfaced only to keep the constructor panic-free. #[allow(clippy::too_many_arguments)] pub fn new( underlying_symbol: String, @@ -192,7 +198,7 @@ impl IronCondor { premium_long_put: Positive, open_fee: Positive, close_fee: Positive, - ) -> Self { + ) -> Result { let mut strategy = IronCondor { name: "Iron Condor".to_string(), kind: StrategyType::IronCondor, @@ -228,9 +234,7 @@ impl IronCondor { None, None, ); - strategy - .add_position(&short_call) - .expect("Invalid short call"); + strategy.add_position(&short_call)?; // Short Put let short_put_option = Options::new( @@ -256,9 +260,7 @@ impl IronCondor { None, None, ); - strategy - .add_position(&short_put) - .expect("Invalid short put"); + strategy.add_position(&short_put)?; // Long Call let long_call_option = Options::new( @@ -284,9 +286,7 @@ impl IronCondor { None, None, ); - strategy - .add_position(&long_call) - .expect("Invalid long call"); + strategy.add_position(&long_call)?; // Long Put let long_put_option = Options::new( @@ -312,12 +312,10 @@ impl IronCondor { None, None, ); - strategy.add_position(&long_put).expect("Invalid long put"); + strategy.add_position(&long_put)?; - strategy - .update_break_even_points() - .expect("Unable to update break even points"); - strategy + strategy.update_break_even_points()?; + Ok(strategy) } } @@ -335,11 +333,12 @@ impl StrategyConstructor for IronCondor { // Sort options by strike price to identify each position let mut sorted_options = vec_positions.to_vec(); + // SAFETY: total order on Positive; f64 fallback to Equal is safe for stable sort sorted_options.sort_by(|a, b| { a.option .strike_price .partial_cmp(&b.option.strike_price) - .unwrap() + .unwrap_or(std::cmp::Ordering::Equal) }); let lowest_strike = &sorted_options[0]; @@ -842,7 +841,7 @@ impl Strategies for IronCondor { let result = (inner_area + outer_triangles) / self.short_call.option.underlying_price.to_f64(); - Ok(Decimal::from_f64(result).unwrap()) + Decimal::from_f64(result).ok_or_else(|| StrategyError::numeric_conversion(result)) } fn get_profit_ratio(&self) -> Result { @@ -903,10 +902,10 @@ impl Optimizable for IronCondor { third: short_call, fourth: long_call, }; - let strategy = strategy.create_strategy(option_chain, &legs); - strategy.validate() - && strategy.get_max_profit().is_ok() - && strategy.get_max_loss().is_ok() + match strategy.create_strategy(option_chain, &legs) { + Ok(s) => s.validate() && s.get_max_profit().is_ok() && s.get_max_loss().is_ok(), + Err(_) => false, + } }) // Map to OptionDataGroup .map(move |(long_put, short_put, short_call, long_call)| { @@ -939,11 +938,24 @@ impl Optimizable for IronCondor { third: short_call, fourth: long_call, }; - let strategy = self.create_strategy(option_chain, &legs); + let strategy = match self.create_strategy(option_chain, &legs) { + Ok(s) => s, + Err(e) => { + tracing::warn!(error = %e, "skipping invalid strategy combination"); + continue; + } + }; // Calculate the current value based on the optimization criteria - let current_value = match criteria { - OptimizationCriteria::Ratio => strategy.get_profit_ratio().unwrap(), - OptimizationCriteria::Area => strategy.get_profit_area().unwrap(), + let metric = match criteria { + OptimizationCriteria::Ratio => strategy.get_profit_ratio(), + OptimizationCriteria::Area => strategy.get_profit_area(), + }; + let current_value = match metric { + Ok(v) => v, + Err(e) => { + tracing::warn!(error = %e, "skipping candidate with unscorable metric"); + continue; + } }; if current_value > best_value { @@ -955,7 +967,18 @@ impl Optimizable for IronCondor { } } - fn create_strategy(&self, chain: &OptionChain, legs: &StrategyLegs) -> Self::Strategy { + /// Constructs an `IronCondor` from the supplied chain and legs. + /// + /// # Errors + /// + /// Returns `StrategyError::OperationError` when the supplied legs are + /// missing required quotes (`short_call.call_bid`, `short_put.put_bid`, + /// `long_call.call_ask`, `long_put.put_ask`) needed to price the strategy. + fn create_strategy( + &self, + chain: &OptionChain, + legs: &StrategyLegs, + ) -> Result { match legs { StrategyLegs::FourLegs { first: long_put, @@ -966,6 +989,32 @@ impl Optimizable for IronCondor { let implied_volatility = short_call.implied_volatility; assert!(implied_volatility <= Positive::ONE); + let short_call_bid = short_call.call_bid.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing call_bid for short call leg", + ) + })?; + let short_put_bid = short_put.put_bid.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing put_bid for short put leg", + ) + })?; + let long_call_ask = long_call.call_ask.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing call_ask for long call leg", + ) + })?; + let long_put_ask = long_put.put_ask.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing put_ask for long put leg", + ) + })?; + + let fee_per_leg = self.get_fees()? / 8.0; IronCondor::new( chain.symbol.clone(), chain.underlying_price, @@ -978,12 +1027,12 @@ impl Optimizable for IronCondor { self.short_call.option.risk_free_rate, self.short_call.option.dividend_yield, self.short_call.option.quantity, - short_call.call_bid.unwrap(), - short_put.put_bid.unwrap(), - long_call.call_ask.unwrap(), - long_put.put_ask.unwrap(), - self.get_fees().unwrap() / 8.0, - self.get_fees().unwrap() / 8.0, + short_call_bid, + short_put_bid, + long_call_ask, + long_put_ask, + fee_per_leg, + fee_per_leg, ) } _ => panic!("Invalid number of legs for Iron Condor strategy"), @@ -1184,7 +1233,8 @@ mod tests_iron_condor { pos_or_panic!(1.8), pos_or_panic!(5.0), pos_or_panic!(5.0), - ); + ) + .unwrap(); assert_eq!(iron_condor.name, "Iron Condor"); assert_eq!(iron_condor.description, IRON_CONDOR_DESCRIPTION.to_string()); @@ -1217,7 +1267,8 @@ mod tests_iron_condor { pos_or_panic!(1.8), pos_or_panic!(5.0), pos_or_panic!(5.0), - ); + ) + .unwrap(); assert_eq!(iron_condor.get_max_loss().unwrap_or(Positive::ZERO), 51.3); } @@ -1243,7 +1294,8 @@ mod tests_iron_condor { pos_or_panic!(2.8), pos_or_panic!(0.07), pos_or_panic!(0.07), - ); + ) + .unwrap(); let expected_profit = iron_condor.get_net_premium_received().unwrap().to_f64(); assert_eq!( @@ -1273,7 +1325,8 @@ mod tests_iron_condor { pos_or_panic!(1.8), pos_or_panic!(5.0), pos_or_panic!(5.0), - ); + ) + .unwrap(); assert_eq!( iron_condor.get_break_even_points().unwrap()[0], @@ -1302,7 +1355,8 @@ mod tests_iron_condor { pos_or_panic!(1.8), pos_or_panic!(5.0), pos_or_panic!(5.0), - ); + ) + .unwrap(); let expected_fees = iron_condor.short_call.open_fee + iron_condor.short_call.close_fee @@ -1336,7 +1390,8 @@ mod tests_iron_condor { pos_or_panic!(1.8), pos_or_panic!(5.0), pos_or_panic!(5.0), - ); + ) + .unwrap(); let price = pos_or_panic!(150.0); let expected_profit = iron_condor @@ -1421,6 +1476,7 @@ mod tests_iron_condor_validable { Positive::ZERO, // open_fee Positive::ZERO, // closing fee ) + .unwrap() } #[test] @@ -1544,6 +1600,7 @@ mod tests_iron_condor_strategies { pos_or_panic!(0.5), // open_fee pos_or_panic!(0.5), // closing fee ) + .unwrap() } #[test] @@ -1643,7 +1700,8 @@ mod tests_iron_condor_strategies { pos_or_panic!(16.8), // premium_long_put pos_or_panic!(0.96), // open_fee pos_or_panic!(0.96), // close_fee - ); + ) + .unwrap(); let break_even_points = condor.get_break_even_points().unwrap(); assert_eq!(break_even_points.len(), 2); @@ -1671,7 +1729,8 @@ mod tests_iron_condor_strategies { pos_or_panic!(10.0), // premium_long_put Positive::ZERO, // open_fee Positive::ZERO, // closing fee - ); + ) + .unwrap(); let max_profit = condor.get_max_profit().unwrap(); assert_eq!(max_profit, pos_or_panic!(ZERO)); } @@ -1696,7 +1755,8 @@ mod tests_iron_condor_strategies { pos_or_panic!(10.0), // premium_long_put pos_or_panic!(0.09), // open_fee pos_or_panic!(0.09), // closing fee - ); + ) + .unwrap(); let max_profit = condor.get_max_profit().unwrap(); assert_eq!(max_profit, pos_or_panic!(19.28)); } @@ -1721,7 +1781,8 @@ mod tests_iron_condor_strategies { pos_or_panic!(11.1), // premium_long_put pos_or_panic!(0.1), // open_fee pos_or_panic!(0.1), // closing fee - ); + ) + .unwrap(); let max_loss = condor.get_max_loss().unwrap(); assert_eq!(max_loss, pos_or_panic!(7.9999999999999964)); } @@ -1746,7 +1807,8 @@ mod tests_iron_condor_strategies { Positive::ONE, pos_or_panic!(0.5), pos_or_panic!(0.5), - ); + ) + .unwrap(); let max_loss = condor.get_max_loss().unwrap(); assert_eq!(max_loss, pos_or_panic!(12.0)); @@ -1785,7 +1847,8 @@ mod tests_iron_condor_strategies { pos_or_panic!(10.0), // premium_long_put Positive::ZERO, // open_fee Positive::ZERO, // closing fee - ); + ) + .unwrap(); assert_eq!(condor.get_net_premium_received().unwrap().to_f64(), ZERO); } @@ -1809,7 +1872,8 @@ mod tests_iron_condor_strategies { pos_or_panic!(10.0), // premium_long_put Positive::ONE, // open_fee Positive::ONE, // closing fee - ); + ) + .unwrap(); assert_eq!(condor.get_net_premium_received().unwrap().to_f64(), 0.0); } @@ -1833,7 +1897,8 @@ mod tests_iron_condor_strategies { pos_or_panic!(10.0), // premium_long_put Positive::ONE, // open_fee Positive::ONE, // closing fee - ); + ) + .unwrap(); assert_eq!(condor.get_net_premium_received().unwrap().to_f64(), 0.0); } @@ -1857,7 +1922,8 @@ mod tests_iron_condor_strategies { pos_or_panic!(10.0), // premium_long_put Positive::ONE, // open_fee Positive::ONE, // closing fee - ); + ) + .unwrap(); assert_eq!(condor.get_net_premium_received().unwrap().to_f64(), 2.0); } @@ -1881,7 +1947,8 @@ mod tests_iron_condor_strategies { pos_or_panic!(20.0), // premium_long_put Positive::ONE, // open_fee Positive::ONE, // closing fee - ); + ) + .unwrap(); assert_eq!(condor.get_net_premium_received().unwrap().to_f64(), 0.0); } @@ -1927,7 +1994,8 @@ mod tests_iron_condor_strategies { Positive::ONE, pos_or_panic!(0.5), pos_or_panic!(0.5), - ); + ) + .unwrap(); assert!(condor.get_max_profit().is_err()); assert_eq!(condor.get_max_loss().unwrap(), pos_or_panic!(14.0)); @@ -1976,6 +2044,7 @@ mod tests_iron_condor_optimizable { pos_or_panic!(0.5), // open_fee pos_or_panic!(0.5), // closing fee ) + .unwrap() } fn create_test_chain() -> OptionChain { @@ -2140,7 +2209,7 @@ mod tests_iron_condor_optimizable { fourth: options[5], // 110.0 strike for long call }; - let new_strategy = condor.create_strategy(&chain, &legs); + let new_strategy = condor.create_strategy(&chain, &legs).unwrap(); assert!(new_strategy.validate()); assert_eq!( new_strategy.long_put.option.strike_price, @@ -2172,6 +2241,8 @@ mod tests_iron_condor_optimizable { second: options[1], }; + // Wrong number of legs is still a panic (that branch is owned by + // issue #292, not panic-free core). let _ = condor.create_strategy(&chain, &legs); } } @@ -2206,6 +2277,7 @@ mod tests_iron_condor_profit { Positive::ZERO, // open_fee Positive::ZERO, // closing fee ) + .unwrap() } #[test] @@ -2342,7 +2414,8 @@ mod tests_iron_condor_profit { Positive::ONE, pos_or_panic!(0.5), // open_fee pos_or_panic!(0.5), // closing fee - ); + ) + .unwrap(); let profit = condor .calculate_profit_at(&Positive::HUNDRED) @@ -2374,7 +2447,8 @@ mod tests_iron_condor_profit { Positive::ONE, pos_or_panic!(0.5), // open_fee pos_or_panic!(0.5), // closing fee - ); + ) + .unwrap(); let profit = condor .calculate_profit_at(&Positive::HUNDRED) @@ -2405,7 +2479,8 @@ mod tests_iron_condor_profit { Positive::ONE, Positive::ZERO, Positive::ZERO, - ); + ) + .unwrap(); let profit = condor .calculate_profit_at(&Positive::HUNDRED) @@ -2469,6 +2544,7 @@ mod tests_iron_condor_delta { pos_or_panic!(0.96), // open_fee pos_or_panic!(0.96), // close_fee ) + .unwrap() } #[test] @@ -2664,6 +2740,7 @@ mod tests_iron_condor_delta_size { pos_or_panic!(0.96), // open_fee pos_or_panic!(0.96), // close_fee ) + .unwrap() } #[test] @@ -2855,6 +2932,7 @@ mod tests_iron_condor_probability { pos_or_panic!(0.96), // open_fee pos_or_panic!(0.96), // close_fee ) + .unwrap() } #[test] @@ -3055,6 +3133,7 @@ mod tests_iron_condor_position_management { pos_or_panic!(0.96), // open_fee pos_or_panic!(0.96), // close_fee ) + .unwrap() } #[test] @@ -3250,6 +3329,7 @@ mod tests_adjust_option_position { pos_or_panic!(0.96), // open_fee pos_or_panic!(0.96), // close_fee ) + .unwrap() } #[test] diff --git a/src/strategies/long_butterfly_spread.rs b/src/strategies/long_butterfly_spread.rs index 807f454d..6a43248f 100644 --- a/src/strategies/long_butterfly_spread.rs +++ b/src/strategies/long_butterfly_spread.rs @@ -114,6 +114,13 @@ impl LongButterflySpread { /// /// # Returns /// A fully configured Long Butterfly Spread strategy with positions and break-even points calculated + /// + /// # Errors + /// + /// Returns `StrategyError` if the break-even calculation fails. In + /// practice this branch is unreachable for a freshly-built long + /// butterfly spread and is surfaced only to keep the constructor + /// panic-free. #[allow(clippy::too_many_arguments)] pub fn new( underlying_symbol: String, @@ -135,7 +142,7 @@ impl LongButterflySpread { close_fee_long_call_low: Positive, open_fee_long_call_high: Positive, close_fee_long_call_high: Positive, - ) -> Self { + ) -> Result { let mut strategy = LongButterflySpread { name: "Long Butterfly".to_string(), kind: StrategyType::LongButterflySpread, @@ -223,11 +230,9 @@ impl LongButterflySpread { strategy.validate(); - strategy - .update_break_even_points() - .expect("Unable to update break even points"); + strategy.update_break_even_points()?; - strategy + Ok(strategy) } } @@ -245,11 +250,12 @@ impl StrategyConstructor for LongButterflySpread { // Sort options by strike price let mut sorted_positions = vec_positions.to_vec(); + // SAFETY: total order on Positive; f64 fallback to Equal is safe for stable sort sorted_positions.sort_by(|a, b| { a.option .strike_price .partial_cmp(&b.option.strike_price) - .unwrap() + .unwrap_or(std::cmp::Ordering::Equal) }); let lower_strike_position = &sorted_positions[0]; @@ -843,10 +849,10 @@ impl Optimizable for LongButterflySpread { second: short, third: long_high, }; - let strategy = strategy.create_strategy(option_chain, &legs); - strategy.validate() - && strategy.get_max_profit().is_ok() - && strategy.get_max_loss().is_ok() + match strategy.create_strategy(option_chain, &legs) { + Ok(s) => s.validate() && s.get_max_profit().is_ok() && s.get_max_loss().is_ok(), + Err(_) => false, + } }) // Map to OptionDataGroup .map(move |(long_low, short, long_high)| { @@ -876,11 +882,24 @@ impl Optimizable for LongButterflySpread { second: short, third: long_high, }; - let strategy = self.create_strategy(option_chain, &legs); + let strategy = match self.create_strategy(option_chain, &legs) { + Ok(s) => s, + Err(e) => { + tracing::warn!(error = %e, "skipping invalid strategy combination"); + continue; + } + }; // Calculate the current value based on the optimization criteria - let current_value = match criteria { - OptimizationCriteria::Ratio => strategy.get_profit_ratio().unwrap(), - OptimizationCriteria::Area => strategy.get_profit_area().unwrap(), + let metric = match criteria { + OptimizationCriteria::Ratio => strategy.get_profit_ratio(), + OptimizationCriteria::Area => strategy.get_profit_area(), + }; + let current_value = match metric { + Ok(v) => v, + Err(e) => { + tracing::warn!(error = %e, "skipping candidate with unscorable metric"); + continue; + } }; if current_value > best_value { @@ -892,7 +911,19 @@ impl Optimizable for LongButterflySpread { } } - fn create_strategy(&self, chain: &OptionChain, legs: &StrategyLegs) -> Self::Strategy { + /// Constructs a `LongButterflySpread` from the supplied chain and legs. + /// + /// # Errors + /// + /// Returns `StrategyError::OperationError` when the supplied legs are + /// missing required quotes (`low_strike.call_ask`, + /// `middle_strike.call_bid`, `high_strike.call_ask`) needed to price the + /// strategy. + fn create_strategy( + &self, + chain: &OptionChain, + legs: &StrategyLegs, + ) -> Result { match legs { StrategyLegs::ThreeLegs { first: low_strike, @@ -902,6 +933,25 @@ impl Optimizable for LongButterflySpread { let implied_volatility = middle_strike.implied_volatility; assert!(implied_volatility <= Positive::ONE); + let low_call_ask = low_strike.call_ask.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing call_ask for low strike leg", + ) + })?; + let middle_call_bid = middle_strike.call_bid.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing call_bid for middle strike leg", + ) + })?; + let high_call_ask = high_strike.call_ask.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing call_ask for high strike leg", + ) + })?; + LongButterflySpread::new( chain.symbol.clone(), chain.underlying_price, @@ -913,9 +963,9 @@ impl Optimizable for LongButterflySpread { self.long_call_low.option.risk_free_rate, self.long_call_low.option.dividend_yield, self.long_call_low.option.quantity, - low_strike.call_ask.unwrap(), - middle_strike.call_bid.unwrap(), - high_strike.call_ask.unwrap(), + low_call_ask, + middle_call_bid, + high_call_ask, self.short_call.open_fee, self.short_call.close_fee, self.long_call_low.open_fee, @@ -1128,6 +1178,7 @@ mod tests_long_butterfly_spread { pos_or_panic!(0.05), // open_fee_long_call_high pos_or_panic!(0.05), // close_fee_long_call_high ) + .unwrap() } #[test] @@ -1229,7 +1280,8 @@ mod tests_long_butterfly_spread { pos_or_panic!(0.05), // close_fee_long_call_low Positive::ONE, // open_fee_long_call_high pos_or_panic!(0.05), // close_fee_long_call_high - ); + ) + .unwrap(); assert_eq!(butterfly.long_call_low.open_fee, 0.05); // fees / 3 assert_eq!(butterfly.short_call.open_fee, 1.0); // fees / 3 @@ -1270,7 +1322,8 @@ mod tests_long_butterfly_spread { pos_or_panic!(0.05), // close_fee_long_call_low pos_or_panic!(0.05), // open_fee_long_call_high pos_or_panic!(0.05), // close_fee_long_call_high - ); + ) + .unwrap(); assert_eq!(butterfly.long_call_low.option.quantity, Positive::TWO); assert_eq!(butterfly.short_call.option.quantity, pos_or_panic!(4.0)); // 2 * 2 @@ -1325,7 +1378,8 @@ mod tests_long_butterfly_spread { pos_or_panic!(0.05), // close_fee_long_call_low pos_or_panic!(1.05), // open_fee_long_call_high pos_or_panic!(0.05), // close_fee_long_call_high - ); + ) + .unwrap(); assert!(check_profit.get_max_profit().is_err()); } } @@ -1383,7 +1437,8 @@ mod tests_long_butterfly_validation { pos_or_panic!(0.05), // close_fee_long_call_low pos_or_panic!(0.05), // open_fee_long_call_high pos_or_panic!(0.05), // close_fee_long_call_high - ); + ) + .unwrap(); assert!(butterfly.validate()); } @@ -1409,7 +1464,8 @@ mod tests_long_butterfly_validation { pos_or_panic!(0.05), // close_fee_long_call_low pos_or_panic!(0.05), // open_fee_long_call_high pos_or_panic!(0.05), // close_fee_long_call_high - ); + ) + .unwrap(); butterfly.long_call_low = create_valid_position(Side::Long, pos_or_panic!(90.0), Positive::ZERO); assert!(!butterfly.validate()); @@ -1437,7 +1493,8 @@ mod tests_long_butterfly_validation { pos_or_panic!(0.05), // close_fee_long_call_low pos_or_panic!(0.05), // open_fee_long_call_high pos_or_panic!(0.05), // close_fee_long_call_high - ); + ) + .unwrap(); assert!(!butterfly.validate()); } @@ -1463,7 +1520,8 @@ mod tests_long_butterfly_validation { pos_or_panic!(0.05), // close_fee_long_call_low pos_or_panic!(0.05), // open_fee_long_call_high pos_or_panic!(0.05), // close_fee_long_call_high - ); + ) + .unwrap(); butterfly.short_call = create_valid_position(Side::Short, Positive::HUNDRED, Positive::ONE); assert!(!butterfly.validate()); } @@ -1490,7 +1548,8 @@ mod tests_long_butterfly_validation { pos_or_panic!(0.05), // close_fee_long_call_low pos_or_panic!(0.05), // open_fee_long_call_high pos_or_panic!(0.05), // close_fee_long_call_high - ); + ) + .unwrap(); butterfly.long_call_high = create_valid_position(Side::Long, pos_or_panic!(110.0), Positive::TWO); assert!(!butterfly.validate()); @@ -1532,6 +1591,7 @@ mod tests_long_butterfly_profit { pos_or_panic!(0.05), // open_fee_long_call_high pos_or_panic!(0.05), // close_fee_long_call_high ) + .unwrap() } #[test] @@ -1607,7 +1667,8 @@ mod tests_long_butterfly_profit { pos_or_panic!(0.05), // close_fee_long_call_low pos_or_panic!(0.05), // open_fee_long_call_high pos_or_panic!(0.05), // close_fee_long_call_high - ); + ) + .unwrap(); let scaled_profit = butterfly .calculate_profit_at(&Positive::HUNDRED) @@ -1653,6 +1714,7 @@ mod tests_long_butterfly_delta { pos_or_panic!(0.05), // open_fee_long_call_high pos_or_panic!(0.05), // close_fee_long_call_high ) + .unwrap() } #[test] @@ -1828,6 +1890,7 @@ mod tests_long_butterfly_delta_size { pos_or_panic!(0.05), // open_fee_long_call_high pos_or_panic!(0.05), // close_fee_long_call_high ) + .unwrap() } #[test] @@ -2000,6 +2063,7 @@ mod tests_long_butterfly_position_management { pos_or_panic!(0.05), // open_fee_long_call_high pos_or_panic!(0.05), // close_fee_long_call_high ) + .unwrap() } #[test] @@ -2179,6 +2243,7 @@ mod tests_adjust_option_position_long { pos_or_panic!(0.05), // open_fee_long_call_high pos_or_panic!(0.05), // close_fee_long_call_high ) + .unwrap() } #[test] @@ -2749,6 +2814,7 @@ mod tests_butterfly_strategies { pos_or_panic!(0.05), // open_fee_long_call_high pos_or_panic!(0.05), // close_fee_long_call_high ) + .unwrap() } #[test] @@ -2841,7 +2907,8 @@ mod tests_butterfly_strategies { Positive::ONE, // close_fee_long_call_low Positive::ONE, // open_fee_long_call_high Positive::ONE, // close_fee_long_call_high - ); + ) + .unwrap(); assert_eq!(butterfly.get_fees().unwrap().to_f64(), 8.0); } @@ -2867,7 +2934,8 @@ mod tests_butterfly_strategies { Positive::ONE, // close_fee_long_call_low Positive::ONE, // open_fee_long_call_high Positive::ONE, // close_fee_long_call_high - ); + ) + .unwrap(); assert_eq!(butterfly.get_fees().unwrap(), pos_or_panic!(16.0)); } @@ -2913,7 +2981,8 @@ mod tests_butterfly_strategies { pos_or_panic!(0.05), // close_fee_long_call_low pos_or_panic!(0.05), // open_fee_long_call_high pos_or_panic!(0.05), // close_fee_long_call_high - ); + ) + .unwrap(); let base_butterfly = create_test_long(); assert_eq!( @@ -2982,6 +3051,7 @@ mod tests_butterfly_optimizable { pos_or_panic!(0.05), // open_fee_long_call_high pos_or_panic!(0.05), // close_fee_long_call_high ) + .unwrap() } #[test] @@ -3070,6 +3140,7 @@ mod tests_butterfly_probability { pos_or_panic!(0.05), // open_fee_long_call_high pos_or_panic!(0.05), // close_fee_long_call_high ) + .unwrap() } mod long_butterfly_tests { @@ -3181,7 +3252,8 @@ mod tests_butterfly_probability { pos_or_panic!(0.05), // close_fee_long_call_low pos_or_panic!(0.05), // open_fee_long_call_high pos_or_panic!(0.05), // close_fee_long_call_high - ); + ) + .unwrap(); info!("=== DEBUGGING USER CASE ==="); diff --git a/src/strategies/long_call.rs b/src/strategies/long_call.rs index dcd3b510..cacaa327 100644 --- a/src/strategies/long_call.rs +++ b/src/strategies/long_call.rs @@ -96,9 +96,11 @@ impl LongCall { /// # Returns /// An initialized instance of `LongCall` strategy configured with the provided parameters. /// - /// # Panics - /// - Panics if adding the long call position to the strategy fails. - /// This typically occurs if the created long call option is invalid. + /// # Errors + /// Returns `StrategyError` if the freshly-constructed long call leg + /// cannot be added to the strategy. In practice this branch is + /// unreachable for a freshly-built single-leg strategy and is surfaced + /// only to keep the constructor panic-free. /// /// # Notes /// - The function relies on creating a default `LongCall` instance and then populating it with positions. @@ -117,7 +119,7 @@ impl LongCall { premium_long_call: Positive, open_fee_long_call: Positive, close_fee_long_call: Positive, - ) -> Self { + ) -> Result { let mut strategy = LongCall::default(); let long_call_option = Options::new( @@ -143,11 +145,9 @@ impl LongCall { None, None, ); - strategy - .add_position(&long_call) - .expect("Invalid long call option"); + strategy.add_position(&long_call)?; - strategy + Ok(strategy) } } @@ -553,7 +553,9 @@ where .template( "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} simulations ({eta})", ) - .expect("Failed to set progress bar template") + .map_err(|e| SimulationError::OtherError { + reason: format!("Failed to set progress bar template: {}", e), + })? .progress_chars("#>-"), ); @@ -642,7 +644,7 @@ where // Calculate expiration premium let mut exp_option = self.long_call.option.clone(); exp_option.underlying_price = final_price; - exp_option.expiration_date = ExpirationDate::Days(Positive::new(0.001).unwrap()); + exp_option.expiration_date = ExpirationDate::Days(Positive::new(0.001)?); expiration_premium = Some(exp_option.calculate_price_black_scholes()?.abs()); expired = true; @@ -782,6 +784,7 @@ mod tests_simulate { Positive::ZERO, Positive::ZERO, ) + .unwrap() } fn create_walk_params(prices: Vec) -> WalkParams { diff --git a/src/strategies/long_put.rs b/src/strategies/long_put.rs index 3d9d1783..726e8a7c 100644 --- a/src/strategies/long_put.rs +++ b/src/strategies/long_put.rs @@ -97,10 +97,11 @@ impl LongPut { /// /// A new instance of the `LongPut` strategy initialized with the provided parameters. /// - /// # Panics - /// - /// This function will panic if: - /// * The `add_position` method fails, which could happen due to invalid configurations of the long put position. + /// # Errors + /// Returns `StrategyError` if the freshly-constructed long put leg + /// cannot be added to the strategy. In practice this branch is + /// unreachable for a freshly-built single-leg strategy and is surfaced + /// only to keep the constructor panic-free. /// #[allow(clippy::too_many_arguments, dead_code)] fn new( @@ -115,7 +116,7 @@ impl LongPut { premium_long_put: Positive, open_fee_long_put: Positive, close_fee_long_put: Positive, - ) -> Self { + ) -> Result { let mut strategy = LongPut::default(); let long_put_option = Options::new( @@ -141,11 +142,9 @@ impl LongPut { None, None, ); - strategy - .add_position(&long_put) - .expect("Invalid long put option"); + strategy.add_position(&long_put)?; - strategy + Ok(strategy) } } @@ -561,7 +560,9 @@ where .template( "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} simulations ({eta})", ) - .expect("Failed to set progress bar template") + .map_err(|e| SimulationError::OtherError { + reason: format!("Failed to set progress bar template: {}", e), + })? .progress_chars("#>-"), ); @@ -650,7 +651,7 @@ where // Calculate expiration premium let mut exp_option = self.long_put.option.clone(); exp_option.underlying_price = final_price; - exp_option.expiration_date = ExpirationDate::Days(Positive::new(0.001).unwrap()); + exp_option.expiration_date = ExpirationDate::Days(Positive::new(0.001)?); expiration_premium = Some(exp_option.calculate_price_black_scholes()?.abs()); expired = true; @@ -792,6 +793,7 @@ mod tests_simulate { Positive::ZERO, Positive::ZERO, ) + .unwrap() } fn create_walk_params(prices: Vec) -> WalkParams { diff --git a/src/strategies/long_straddle.rs b/src/strategies/long_straddle.rs index e6148cd4..e382ffd5 100644 --- a/src/strategies/long_straddle.rs +++ b/src/strategies/long_straddle.rs @@ -150,6 +150,12 @@ impl LongStraddle { /// # Returns /// A fully initialized Long Straddle strategy with calculated break-even points /// + /// # Errors + /// + /// Returns `StrategyError` if either freshly-constructed leg cannot be + /// added to the strategy or if the break-even calculation fails. In + /// practice these branches are unreachable for a freshly-built long + /// straddle and are surfaced only to keep the constructor panic-free. #[allow(clippy::too_many_arguments)] pub fn new( underlying_symbol: String, @@ -166,7 +172,7 @@ impl LongStraddle { close_fee_long_call: Positive, open_fee_long_put: Positive, close_fee_long_put: Positive, - ) -> Self { + ) -> Result { if strike == Positive::ZERO { strike = underlying_price; } @@ -203,9 +209,7 @@ impl LongStraddle { None, None, ); - strategy - .add_position(&long_call) - .expect("Invalid long call"); + strategy.add_position(&long_call)?; let long_put_option = Options::new( OptionType::European, @@ -230,12 +234,10 @@ impl LongStraddle { None, None, ); - strategy.add_position(&long_put).expect("Invalid long put"); + strategy.add_position(&long_put)?; - strategy - .update_break_even_points() - .expect("Unable to update break even points"); - strategy + strategy.update_break_even_points()?; + Ok(strategy) } } @@ -613,7 +615,7 @@ impl Strategies for LongStraddle { let cat = (strike_diff / 2.0_f64.sqrt()).to_f64(); let loss_area = (cat.powf(2.0)) / (2.0 * 10.0_f64.powf(cat.log10().ceil())); let result = (1.0 / loss_area) * 10000.0; // Invert the value to get the profit area: the lower, the better - Ok(Decimal::from_f64(result).unwrap()) + Decimal::from_f64(result).ok_or_else(|| StrategyError::numeric_conversion(result)) } fn get_profit_ratio(&self) -> Result { @@ -622,7 +624,7 @@ impl Strategies for LongStraddle { Ok(max_loss) => ((break_even_diff / max_loss) * 100.0).to_f64(), Err(_) => ZERO, }; - Ok(Decimal::from_f64(result).unwrap()) + Decimal::from_f64(result).ok_or_else(|| StrategyError::numeric_conversion(result)) } } @@ -671,10 +673,10 @@ impl Optimizable for LongStraddle { first: both, second: both, }; - let strategy = strategy.create_strategy(option_chain, &legs); - strategy.validate() - && strategy.get_max_profit().is_ok() - && strategy.get_max_loss().is_ok() + match strategy.create_strategy(option_chain, &legs) { + Ok(s) => s.validate() && s.get_max_profit().is_ok() && s.get_max_loss().is_ok(), + Err(_) => false, + } }) // Map to OptionDataGroup .map(OptionDataGroup::One) @@ -701,11 +703,24 @@ impl Optimizable for LongStraddle { first: both, second: both, }; - let strategy = self.create_strategy(option_chain, &legs); + let strategy = match self.create_strategy(option_chain, &legs) { + Ok(s) => s, + Err(e) => { + tracing::warn!(error = %e, "skipping invalid strategy combination"); + continue; + } + }; // Calculate the current value based on the optimization criteria - let current_value = match criteria { - OptimizationCriteria::Ratio => strategy.get_profit_ratio().unwrap(), - OptimizationCriteria::Area => strategy.get_profit_area().unwrap(), + let metric = match criteria { + OptimizationCriteria::Ratio => strategy.get_profit_ratio(), + OptimizationCriteria::Area => strategy.get_profit_area(), + }; + let current_value = match metric { + Ok(v) => v, + Err(e) => { + tracing::warn!(error = %e, "skipping candidate with unscorable metric"); + continue; + } }; if current_value > best_value { @@ -717,13 +732,36 @@ impl Optimizable for LongStraddle { } } - fn create_strategy(&self, chain: &OptionChain, legs: &StrategyLegs) -> Self::Strategy { + /// Constructs a `LongStraddle` from the supplied chain and legs. + /// + /// # Errors + /// + /// Returns `StrategyError::OperationError` when the supplied legs are + /// missing required quotes (`call.call_ask`, `put.put_ask`) needed to + /// price the strategy. + fn create_strategy( + &self, + chain: &OptionChain, + legs: &StrategyLegs, + ) -> Result { let (call, put) = match legs { StrategyLegs::TwoLegs { first, second } => (first, second), _ => panic!("Invalid number of legs for this strategy"), }; let implied_volatility = call.implied_volatility; assert!(implied_volatility <= Positive::ONE); + let call_ask = call.call_ask.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing call_ask for long call leg", + ) + })?; + let put_ask = put.put_ask.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing put_ask for long put leg", + ) + })?; LongStraddle::new( chain.symbol.clone(), chain.underlying_price, @@ -733,8 +771,8 @@ impl Optimizable for LongStraddle { self.long_call.option.risk_free_rate, self.long_call.option.dividend_yield, self.long_call.option.quantity, - call.call_ask.unwrap(), - put.put_ask.unwrap(), + call_ask, + put_ask, self.long_call.open_fee, self.long_call.close_fee, self.long_put.open_fee, @@ -907,6 +945,7 @@ mod tests_long_straddle_probability { Positive::ZERO, // open_fee_long_put Positive::ZERO, // close_fee_long_put ) + .unwrap() } #[test] @@ -1056,6 +1095,7 @@ mod tests_long_straddle_delta { pos_or_panic!(7.01), // open_fee_long_put pos_or_panic!(7.01), // close_fee_long_put ) + .unwrap() } #[test] @@ -1201,6 +1241,7 @@ mod tests_long_straddle_delta_size { pos_or_panic!(7.01), // open_fee_long_put pos_or_panic!(7.01), // close_fee_long_put ) + .unwrap() } #[test] @@ -1342,6 +1383,7 @@ mod tests_straddle_position_management { pos_or_panic!(0.1), // open_fee_long_put pos_or_panic!(0.1), // close_fee_long_put ) + .unwrap() } #[test] @@ -1451,6 +1493,7 @@ mod tests_adjust_option_position { pos_or_panic!(0.1), // open_fee_long_put pos_or_panic!(0.1), // close_fee_long_put ) + .unwrap() } #[test] diff --git a/src/strategies/long_strangle.rs b/src/strategies/long_strangle.rs index ecdb3396..69877b9d 100644 --- a/src/strategies/long_strangle.rs +++ b/src/strategies/long_strangle.rs @@ -159,6 +159,13 @@ impl LongStrangle { /// /// Returns a fully initialized `LongStrangle` strategy with properly configured positions and calculated /// break-even points. + /// + /// # Errors + /// + /// Returns `StrategyError` if either freshly-constructed leg cannot be + /// added to the strategy or if the break-even calculation fails. In + /// practice these branches are unreachable for a freshly-built + /// strangle and are surfaced only to keep the constructor panic-free. #[allow(clippy::too_many_arguments)] pub fn new( underlying_symbol: String, @@ -176,7 +183,7 @@ impl LongStrangle { close_fee_long_call: Positive, open_fee_long_put: Positive, close_fee_long_put: Positive, - ) -> Self { + ) -> Result { if call_strike == Positive::ZERO { call_strike = underlying_price * 1.1; } @@ -215,7 +222,7 @@ impl LongStrangle { None, None, ); - strategy.add_position(&long_call).expect("Invalid position"); + strategy.add_position(&long_call)?; let long_put_option = Options::new( OptionType::European, @@ -240,13 +247,11 @@ impl LongStrangle { None, None, ); - strategy.add_position(&long_put).expect("Invalid position"); + strategy.add_position(&long_put)?; - strategy - .update_break_even_points() - .expect("Unable to update break even points"); + strategy.update_break_even_points()?; - strategy + Ok(strategy) } } @@ -264,11 +269,12 @@ impl StrategyConstructor for LongStrangle { // Sort options by option style to identify call and put let mut sorted_positions = vec_positions.to_vec(); + // SAFETY: total order on Positive; f64 fallback to Equal is safe for stable sort sorted_positions.sort_by(|a, b| { a.option .strike_price .partial_cmp(&b.option.strike_price) - .unwrap() + .unwrap_or(std::cmp::Ordering::Equal) }); let put_position = &sorted_positions[0]; // Put will be first @@ -637,7 +643,7 @@ impl Strategies for LongStrangle { let loss_area = ((inner_square + triangles) / self.long_call.option.underlying_price).to_f64(); let result = 1.0 / loss_area; // Invert the value to get the profit area: the lower, the better - Ok(Decimal::from_f64(result).unwrap()) + Decimal::from_f64(result).ok_or_else(|| StrategyError::numeric_conversion(result)) } fn get_profit_ratio(&self) -> Result { let max_loss = self.get_max_loss().unwrap_or(Positive::ZERO); @@ -647,7 +653,7 @@ impl Strategies for LongStrangle { let break_even_diff = self.break_even_points[1] - self.break_even_points[0]; let ratio = max_loss / break_even_diff * 100.0; let result = 1.0 / ratio; // Invert the value to get the profit ratio: the lower, the better - Ok(Decimal::from_f64(result).unwrap()) + Decimal::from_f64(result).ok_or_else(|| StrategyError::numeric_conversion(result)) } fn get_best_range_to_show(&self, step: Positive) -> Result, StrategyError> { let (first_option, last_option) = (self.break_even_points[0], self.break_even_points[1]); @@ -690,10 +696,10 @@ impl Optimizable for LongStrangle { FindOptimalSide::DeltaRange(min, max) => { let (_, delta_put) = long_put.current_deltas(); let (delta_call, _) = long_call.current_deltas(); - delta_put.unwrap() > min - && delta_put.unwrap() < max - && delta_call.unwrap() > min - && delta_call.unwrap() < max + let (Some(dp), Some(dc)) = (delta_put, delta_call) else { + return false; + }; + dp > min && dp < max && dc > min && dc < max } FindOptimalSide::Center => { long_put.is_valid_optimal_side(underlying_price, &FindOptimalSide::Lower) @@ -718,10 +724,10 @@ impl Optimizable for LongStrangle { second: long_call, }; - let strategy = strategy.create_strategy(option_chain, &legs); - strategy.validate() - && strategy.get_max_profit().is_ok() - && strategy.get_max_loss().is_ok() + match strategy.create_strategy(option_chain, &legs) { + Ok(s) => s.validate() && s.get_max_profit().is_ok() && s.get_max_loss().is_ok(), + Err(_) => false, + } }) // Map to OptionDataGroup .map(move |(long_put, long_call)| OptionDataGroup::Two(long_put, long_call)) @@ -748,11 +754,24 @@ impl Optimizable for LongStrangle { first: long_put, second: long_call, }; - let strategy = self.create_strategy(option_chain, &legs); + let strategy = match self.create_strategy(option_chain, &legs) { + Ok(s) => s, + Err(e) => { + tracing::warn!(error = %e, "skipping invalid strategy combination"); + continue; + } + }; // Calculate the current value based on the optimization criteria - let current_value = match criteria { - OptimizationCriteria::Ratio => strategy.get_profit_ratio().unwrap(), - OptimizationCriteria::Area => strategy.get_profit_area().unwrap(), + let metric = match criteria { + OptimizationCriteria::Ratio => strategy.get_profit_ratio(), + OptimizationCriteria::Area => strategy.get_profit_area(), + }; + let current_value = match metric { + Ok(v) => v, + Err(e) => { + tracing::warn!(error = %e, "skipping candidate with unscorable metric"); + continue; + } }; if current_value > best_value { @@ -773,13 +792,36 @@ impl Optimizable for LongStrangle { && long_put.put_bid.unwrap_or(Positive::ZERO) > Positive::ZERO } - fn create_strategy(&self, chain: &OptionChain, legs: &StrategyLegs) -> Self::Strategy { + /// Constructs a `LongStrangle` from the supplied chain and legs. + /// + /// # Errors + /// + /// Returns `StrategyError::OperationError` when the supplied legs are + /// missing required quotes (`call.call_ask`, `put.put_ask`) needed to + /// price the strategy. + fn create_strategy( + &self, + chain: &OptionChain, + legs: &StrategyLegs, + ) -> Result { let (put, call) = match legs { StrategyLegs::TwoLegs { first, second } => (first, second), _ => panic!("Invalid number of legs for this strategy"), }; let implied_volatility = call.implied_volatility; assert!(implied_volatility <= Positive::ONE); + let call_ask = call.call_ask.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing call_ask for long call leg", + ) + })?; + let put_ask = put.put_ask.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing put_ask for long put leg", + ) + })?; LongStrangle::new( chain.symbol.clone(), chain.underlying_price, @@ -790,8 +832,8 @@ impl Optimizable for LongStrangle { self.long_call.option.risk_free_rate, self.long_call.option.dividend_yield, self.long_call.option.quantity, - call.call_ask.unwrap(), - put.put_ask.unwrap(), + call_ask, + put_ask, self.long_call.open_fee, self.long_call.close_fee, self.long_put.open_fee, @@ -957,8 +999,8 @@ impl PnLCalculator for LongStrangle { PnL { realized: None, unrealized: None, - initial_costs: position.total_cost().unwrap(), - initial_income: position.premium_received().unwrap(), + initial_costs: position.total_cost()?, + initial_income: position.premium_received()?, date_time: Utc::now(), } } @@ -970,8 +1012,8 @@ impl PnLCalculator for LongStrangle { PnL { realized: None, unrealized: None, - initial_costs: position.total_cost().unwrap(), - initial_income: position.premium_received().unwrap(), + initial_costs: position.total_cost()?, + initial_income: position.premium_received()?, date_time: Utc::now(), } } @@ -987,14 +1029,14 @@ impl PnLCalculator for LongStrangle { match (side, option_style) { (Side::Long, OptionStyle::Call) => { let mut position = self.long_call.clone(); - position.option.side = Side::Short; // Sell the call + position.option.side = Side::Short; // Sell the call position.option.quantity = *quantity; position.option.strike_price = *strike; PnL { realized: None, unrealized: None, - initial_costs: position.total_cost().unwrap(), - initial_income: position.premium_received().unwrap(), + initial_costs: position.total_cost()?, + initial_income: position.premium_received()?, date_time: Utc::now(), } } @@ -1006,8 +1048,8 @@ impl PnLCalculator for LongStrangle { PnL { realized: None, unrealized: None, - initial_costs: position.total_cost().unwrap(), - initial_income: position.premium_received().unwrap(), + initial_costs: position.total_cost()?, + initial_income: position.premium_received()?, date_time: Utc::now(), } } @@ -1050,6 +1092,7 @@ mod tests_long_strangle_probability { Positive::ZERO, // open_fee_long_put Positive::ZERO, // close_fee_long_put ) + .unwrap() } #[test] @@ -1201,6 +1244,7 @@ mod tests_long_strangle_delta { pos_or_panic!(7.01), // open_fee_long_put pos_or_panic!(7.01), // close_fee_long_put ) + .unwrap() } #[test] @@ -1344,6 +1388,7 @@ mod tests_long_strangle_delta_size { pos_or_panic!(7.01), // open_fee_long_put pos_or_panic!(7.01), // close_fee_long_put ) + .unwrap() } #[test] @@ -1640,6 +1685,7 @@ mod tests_strangle_position_management { pos_or_panic!(0.1), // open_fee_long_put pos_or_panic!(0.1), // close_fee_long_put ) + .unwrap() } #[test] @@ -1750,6 +1796,7 @@ mod tests_adjust_option_position_long { pos_or_panic!(0.1), // open_fee_long_put pos_or_panic!(0.1), // close_fee_long_put ) + .unwrap() } #[test] diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs index bf2d6f13..8ddc4974 100644 --- a/src/strategies/mod.rs +++ b/src/strategies/mod.rs @@ -65,7 +65,7 @@ //! pos_or_panic!(0.78), // open_fee_long //! pos_or_panic!(0.73), // close_fee_long //! pos_or_panic!(0.73), // close_fee_short -//! ); +//! ).expect("valid bull call spread"); //! //! let profit = spread.get_max_profit().unwrap_or(Positive::ZERO); //! let loss = spread.get_max_loss().unwrap_or(Positive::ZERO); @@ -180,7 +180,7 @@ //! pos_or_panic!(1.8), // premium_long_put //! pos_or_panic!(5.0), // open_fee //! pos_or_panic!(5.0), // close_fee -//! ); +//! ).expect("valid iron condor"); //! //! let max_profit = condor.get_max_profit().unwrap_or(Positive::ZERO); //! let max_loss = condor.get_max_loss().unwrap_or(Positive::ZERO); diff --git a/src/strategies/poor_mans_covered_call.rs b/src/strategies/poor_mans_covered_call.rs index 6ddd8fd6..4fa468dd 100644 --- a/src/strategies/poor_mans_covered_call.rs +++ b/src/strategies/poor_mans_covered_call.rs @@ -174,6 +174,12 @@ impl PoorMansCoveredCall { /// * The investor wants to generate income from the short calls while maintaining upside potential /// * The investor seeks a capital-efficient alternative to traditional covered calls /// + /// # Errors + /// + /// Returns `StrategyError` if either freshly-constructed leg cannot be + /// added to the strategy or if the break-even calculation fails. In + /// practice these branches are unreachable for a freshly-built PMCC and + /// are surfaced only to keep the constructor panic-free. #[allow(clippy::too_many_arguments)] pub fn new( underlying_symbol: String, @@ -192,7 +198,7 @@ impl PoorMansCoveredCall { close_fee_long_call: Positive, open_fee_short_call: Positive, close_fee_short_call: Positive, - ) -> Self { + ) -> Result { let mut strategy = PoorMansCoveredCall::default(); // Long Call (LEAPS) @@ -219,9 +225,7 @@ impl PoorMansCoveredCall { None, None, ); - strategy - .add_position(&long_call) - .expect("Invalid long call option"); + strategy.add_position(&long_call)?; // Short Call let short_call_option = Options::new( @@ -247,14 +251,10 @@ impl PoorMansCoveredCall { None, None, ); - strategy - .add_position(&short_call) - .expect("Invalid short call option"); + strategy.add_position(&short_call)?; - strategy - .update_break_even_points() - .expect("Unable to update break even points"); - strategy + strategy.update_break_even_points()?; + Ok(strategy) } } @@ -272,11 +272,12 @@ impl StrategyConstructor for PoorMansCoveredCall { // Sort options by strike price to identify long and short positions let mut sorted_positions = vec_positions.to_vec(); + // SAFETY: total order on Positive; f64 fallback to Equal is safe for stable sort sorted_positions.sort_by(|a, b| { a.option .strike_price .partial_cmp(&b.option.strike_price) - .unwrap() + .unwrap_or(std::cmp::Ordering::Equal) }); let lower_strike_position = &sorted_positions[0]; @@ -647,7 +648,7 @@ impl Strategies for PoorMansCoveredCall { .to_f64(); let high = self.get_max_profit().unwrap_or(Positive::ZERO).to_f64(); let result = base * high / 200.0; - Ok(Decimal::from_f64(result).unwrap()) + Decimal::from_f64(result).ok_or_else(|| StrategyError::numeric_conversion(result)) } fn get_profit_ratio(&self) -> Result { @@ -655,7 +656,7 @@ impl Strategies for PoorMansCoveredCall { (Ok(profit), Ok(loss)) => (profit / loss).to_f64() * 100.0, _ => ZERO, }; - Ok(Decimal::from_f64(result).unwrap()) + Decimal::from_f64(result).ok_or_else(|| StrategyError::numeric_conversion(result)) } } @@ -704,16 +705,33 @@ impl Optimizable for PoorMansCoveredCall { first: long_call_option, second: short_call_option, }; - let strategy: PoorMansCoveredCall = self.create_strategy(option_chain, &legs); + let strategy: PoorMansCoveredCall = match self.create_strategy(option_chain, &legs) + { + Ok(s) => s, + Err(e) => { + tracing::warn!( + error = %e, + "skipping invalid strategy combination" + ); + continue; + } + }; if !strategy.validate() { debug!("Invalid strategy"); continue; } - let current_value = match criteria { - OptimizationCriteria::Ratio => strategy.get_profit_ratio().unwrap(), - OptimizationCriteria::Area => strategy.get_profit_area().unwrap(), + let metric = match criteria { + OptimizationCriteria::Ratio => strategy.get_profit_ratio(), + OptimizationCriteria::Area => strategy.get_profit_area(), + }; + let current_value = match metric { + Ok(v) => v, + Err(e) => { + tracing::warn!(error = %e, "skipping candidate with unscorable metric"); + continue; + } }; if current_value > best_value { @@ -724,7 +742,18 @@ impl Optimizable for PoorMansCoveredCall { } } - fn create_strategy(&self, chain: &OptionChain, legs: &StrategyLegs) -> Self::Strategy { + /// Constructs a `PoorMansCoveredCall` from the supplied chain and legs. + /// + /// # Errors + /// + /// Returns `StrategyError::OperationError` when the supplied legs are + /// missing required quotes (`long.call_ask`, `short.call_bid`) needed to + /// price the strategy. + fn create_strategy( + &self, + chain: &OptionChain, + legs: &StrategyLegs, + ) -> Result { let (long, short) = match legs { StrategyLegs::TwoLegs { first, second } => (first, second), _ => panic!("Invalid number of legs for this strategy"), @@ -732,6 +761,19 @@ impl Optimizable for PoorMansCoveredCall { let implied_volatility = short.implied_volatility; assert!(implied_volatility <= Positive::ONE); + let long_call_ask = long.call_ask.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing call_ask for long call leg", + ) + })?; + let short_call_bid = short.call_bid.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing call_bid for short call leg", + ) + })?; + PoorMansCoveredCall::new( chain.symbol.clone(), chain.underlying_price, @@ -743,8 +785,8 @@ impl Optimizable for PoorMansCoveredCall { self.short_call.option.risk_free_rate, self.short_call.option.dividend_yield, self.short_call.option.quantity, - long.call_ask.unwrap(), - short.call_bid.unwrap(), + long_call_ask, + short_call_bid, self.long_call.open_fee, self.long_call.close_fee, self.short_call.open_fee, @@ -886,6 +928,7 @@ mod tests_pmcc_validation { pos_or_panic!(0.5), pos_or_panic!(0.5), ) + .unwrap() } #[test] @@ -1055,6 +1098,7 @@ mod tests_pmcc_optimization { pos_or_panic!(0.5), pos_or_panic!(0.5), ) + .unwrap() } #[test] @@ -1211,6 +1255,7 @@ mod tests_pmcc_pnl { pos_or_panic!(0.5), pos_or_panic!(0.5), ) + .unwrap() } #[test] @@ -1304,7 +1349,8 @@ mod tests_pmcc_best_area { Positive::ONE, pos_or_panic!(0.5), pos_or_panic!(0.5), - ); + ) + .unwrap(); Ok((strategy, option_chain)) } @@ -1381,7 +1427,8 @@ mod tests_pmcc_best_ratio { Positive::ONE, pos_or_panic!(0.5), pos_or_panic!(0.5), - ); + ) + .unwrap(); Ok((strategy, option_chain)) } @@ -1458,6 +1505,7 @@ mod tests_short_straddle_delta { pos_or_panic!(7.01), // open_fee_long_call pos_or_panic!(7.01), // close_fee_long_call ) + .unwrap() } #[test] @@ -1603,6 +1651,7 @@ mod tests_short_straddle_delta_size { pos_or_panic!(7.01), // open_fee_long_call pos_or_panic!(7.01), // close_fee_long_call ) + .unwrap() } #[test] @@ -1744,6 +1793,7 @@ mod tests_poor_mans_covered_call_probability { pos_or_panic!(0.85), // open_fee_long_call pos_or_panic!(0.85), // close_fee_long_call ) + .unwrap() } #[test] @@ -1954,6 +2004,7 @@ mod tests_poor_mans_covered_call_position_management { pos_or_panic!(0.85), // open_fee_long_call pos_or_panic!(0.85), // close_fee_long_call ) + .unwrap() } #[test] @@ -2082,6 +2133,7 @@ mod tests_adjust_option_position { pos_or_panic!(0.85), // open_fee_long_call pos_or_panic!(0.85), // close_fee_long_call ) + .unwrap() } #[test] diff --git a/src/strategies/probabilities/core.rs b/src/strategies/probabilities/core.rs index 44545dd1..3b037fad 100644 --- a/src/strategies/probabilities/core.rs +++ b/src/strategies/probabilities/core.rs @@ -10,6 +10,7 @@ use positive::{Positive, pos_or_panic}; use crate::error::probability::ProbabilityError; +use crate::error::strategies::StrategyError; use crate::model::ProfitLossRange; use crate::pricing::payoff::Profit; use crate::strategies::base::Strategies; @@ -68,7 +69,7 @@ pub trait ProbabilityAnalysis: Strategies + Profit { volatility_adj: Option, trend: Option, ) -> Result { - let break_even_points = self.get_break_even_points().unwrap(); + let break_even_points = self.get_break_even_points()?; // If both parameters are None, return default probabilities based on profit ranges if volatility_adj.is_none() && trend.is_none() { let probability_of_profit = self.probability_of_profit(None, None)?; @@ -80,7 +81,7 @@ pub trait ProbabilityAnalysis: Strategies + Profit { probability_of_max_loss: Positive::ZERO, // Default value when no volatility adjustment expected_value, break_even_points: break_even_points.to_vec(), - risk_reward_ratio: Positive::new_decimal(self.get_profit_ratio().unwrap()) + risk_reward_ratio: Positive::new_decimal(self.get_profit_ratio()?) .unwrap_or(Positive::ZERO), }); } @@ -92,7 +93,7 @@ pub trait ProbabilityAnalysis: Strategies + Profit { let (prob_max_profit, prob_max_loss) = self.calculate_extreme_probabilities(volatility_adj, trend)?; let risk_reward_ratio = - Positive::new_decimal(self.get_profit_ratio().unwrap()).unwrap_or(Positive::ZERO); + Positive::new_decimal(self.get_profit_ratio()?).unwrap_or(Positive::ZERO); Ok(StrategyProbabilityAnalysis { probability_of_profit, @@ -148,8 +149,10 @@ pub trait ProbabilityAnalysis: Strategies + Profit { } let step = self.get_underlying_price() / 100.0; - let range = self.get_best_range_to_show(step).unwrap(); - let expiration = *self.get_expiration().values().next().unwrap(); + let range = self.get_best_range_to_show(step)?; + let expiration = *self.get_expiration().values().next().ok_or_else(|| { + StrategyError::empty_collection("expected_value: no expiration on strategy") + })?; let mut probabilities = Vec::with_capacity(range.len()); let mut last_prob = Decimal::ZERO; @@ -169,13 +172,14 @@ pub trait ProbabilityAnalysis: Strategies + Profit { last_prob = prob.0.to_dec(); } - let expected_value = - range - .iter() - .zip(probabilities.iter()) - .fold(0.0, |acc, (price, prob)| { - acc + self.calculate_profit_at(price).unwrap().to_f64().unwrap() * *prob - }); + let mut expected_value = 0.0_f64; + for (price, prob) in range.iter().zip(probabilities.iter()) { + let profit_dec = self.calculate_profit_at(price)?; + let profit = profit_dec + .to_f64() + .ok_or_else(|| StrategyError::numeric_conversion(0.0))?; + expected_value += profit * prob.to_f64(); + } let total_prob: f64 = probabilities.iter().map(|p| p.to_f64()).sum(); if (total_prob - 1.0).abs() > 0.05 { @@ -293,8 +297,16 @@ pub trait ProbabilityAnalysis: Strategies + Profit { .find(|range| range.upper_bound.is_none()); let max_loss_range = loss_ranges.iter().find(|range| range.lower_bound.is_none()); - let expiration = *self.get_expiration().values().next().unwrap(); - let risk_free_rate = *self.get_risk_free_rate().values().next().unwrap(); + let expiration = *self.get_expiration().values().next().ok_or_else(|| { + StrategyError::empty_collection( + "calculate_extreme_probabilities: no expiration on strategy", + ) + })?; + let risk_free_rate = *self.get_risk_free_rate().values().next().ok_or_else(|| { + StrategyError::empty_collection( + "calculate_extreme_probabilities: no risk_free_rate on strategy", + ) + })?; let underlying_price = self.get_underlying_price(); let mut max_profit_prob = Positive::ZERO; @@ -375,6 +387,7 @@ mod tests_probability_analysis { pos_or_panic!(0.55), // close_fee_short pos_or_panic!(0.54), // open_fee_short ) + .unwrap() } #[test] @@ -528,6 +541,7 @@ mod tests_expected_value { pos_or_panic!(0.55), // close_fee_short pos_or_panic!(0.54), // open_fee_short ) + .unwrap() } #[test] diff --git a/src/strategies/probabilities/mod.rs b/src/strategies/probabilities/mod.rs index cabe5952..7bf51b6f 100644 --- a/src/strategies/probabilities/mod.rs +++ b/src/strategies/probabilities/mod.rs @@ -57,6 +57,7 @@ use positive::pos_or_panic; //! ### Basic Strategy Analysis //! //! ```rust +//! # fn run() -> Result<(), Box> { //! use rust_decimal_macros::dec; //! use tracing::info; //! use optionstratlib::model::types::{ OptionStyle, OptionType, Side}; @@ -81,15 +82,18 @@ use positive::pos_or_panic; //! pos_or_panic!(0.78), // open_fee_long //! pos_or_panic!(0.73), // close_fee_long //! pos_or_panic!(0.73), // close_fee_short -//! ); -//! let analysis = strategy.analyze_probabilities(None, None); +//! )?; +//! let analysis = strategy.analyze_probabilities(None, None)?; //! //! info!("Analysis: {:?}", analysis); +//! # Ok(()) +//! # } //! ``` //! //! ### Analysis with Volatility Adjustment //! //! ```rust +//! # fn run() -> Result<(), Box> { //! use rust_decimal_macros::dec; //! use optionstratlib::ExpirationDate; //! use optionstratlib::strategies::probabilities::{ProbabilityAnalysis, VolatilityAdjustment}; @@ -113,19 +117,22 @@ use positive::pos_or_panic; //! pos_or_panic!(0.78), // open_fee_long //! pos_or_panic!(0.73), // close_fee_long //! pos_or_panic!(0.73), // close_fee_short -//! ); +//! )?; //! //! let vol_adj = Some(VolatilityAdjustment { //! base_volatility: pos_or_panic!(0.20), // 20% base volatility //! std_dev_adjustment: pos_or_panic!(0.10), // 10% adjustment //! }); //! -//! let analysis = strategy.analyze_probabilities(vol_adj, None); +//! let _analysis = strategy.analyze_probabilities(vol_adj, None)?; +//! # Ok(()) +//! # } //! ``` //! //! ### Analysis with Price Trend //! //! ```rust +//! # fn run() -> Result<(), Box> { //! use rust_decimal_macros::dec; //! use optionstratlib::ExpirationDate; //! use positive::Positive; @@ -148,18 +155,21 @@ use positive::pos_or_panic; //! pos_or_panic!(0.78), // open_fee_long //! pos_or_panic!(0.73), // close_fee_long //! pos_or_panic!(0.73), // close_fee_short -//! ); +//! )?; //! let trend = Some(PriceTrend { //! drift_rate: 0.05, // 5% annual drift //! confidence: 0.95, // 95% confidence level //! }); //! -//! let analysis = strategy.analyze_probabilities(None, trend).unwrap(); +//! let _analysis = strategy.analyze_probabilities(None, trend)?; +//! # Ok(()) +//! # } //! ``` //! //! ### Price Range Probability Analysis //! //! ```rust +//! # fn run() -> Result<(), Box> { //! use tracing::info; //! use optionstratlib::strategies::probabilities::calculate_price_probability; //! use optionstratlib::ExpirationDate; @@ -174,8 +184,10 @@ use positive::pos_or_panic; //! None, // trend //! &ExpirationDate::Days(pos_or_panic!(30.0)), //! None // risk-free rate -//! ).unwrap(); +//! )?; //! info!("Probabilities: {}, {}, {}", prob_below, prob_in_range, prob_above); +//! # Ok(()) +//! # } //! ``` //! //! ## Mathematical Models diff --git a/src/strategies/probabilities/utils.rs b/src/strategies/probabilities/utils.rs index ea437268..980c8ccd 100644 --- a/src/strategies/probabilities/utils.rs +++ b/src/strategies/probabilities/utils.rs @@ -110,9 +110,20 @@ pub fn calculate_single_point_probability( }, )); } - risk_free.to_f64().unwrap() + (t.drift_rate * t.confidence) + let rf = risk_free.to_f64().ok_or_else(|| { + ProbabilityError::StdError(format!( + "calculate_single_point_probability: risk_free Decimal {} not representable as f64", + risk_free + )) + })?; + rf + (t.drift_rate * t.confidence) } - None => risk_free.to_f64().unwrap(), + None => risk_free.to_f64().ok_or_else(|| { + ProbabilityError::StdError(format!( + "calculate_single_point_probability: risk_free Decimal {} not representable as f64", + risk_free + )) + })?, }; // Calculate parameters for the log-normal distribution @@ -120,12 +131,10 @@ pub fn calculate_single_point_probability( let std_dev = volatility * time_to_expiry.sqrt(); // Calculate z-score considering drift - let z_score: Decimal = - f2du!((log_ratio.to_f64() - drift_rate * time_to_expiry) / std_dev).unwrap(); + let z_score: Decimal = f2du!((log_ratio.to_f64() - drift_rate * time_to_expiry) / std_dev)?; // Calculate probabilities using the standard normal distribution - let prob_below: Positive = - Positive::new_decimal(big_n(z_score).unwrap()).unwrap_or(Positive::ZERO); + let prob_below: Positive = Positive::new_decimal(big_n(z_score)?).unwrap_or(Positive::ZERO); let prob_above: Positive = Positive::new(1.0 - prob_below.to_f64()).unwrap_or(Positive::ZERO); Ok((prob_below, prob_above)) diff --git a/src/strategies/protective_put.rs b/src/strategies/protective_put.rs index c6488bc7..1b714c7b 100644 --- a/src/strategies/protective_put.rs +++ b/src/strategies/protective_put.rs @@ -61,8 +61,13 @@ pub struct ProtectivePut { impl ProtectivePut { /// Creates a new Protective Put strategy. + /// + /// # Errors + /// + /// Returns `StrategyError` if the break-even calculation fails. In + /// practice this branch is unreachable for a freshly-built protective + /// put and is surfaced only to keep the constructor panic-free. #[allow(clippy::too_many_arguments)] - #[must_use] pub fn new( underlying_symbol: String, underlying_price: Positive, @@ -77,7 +82,7 @@ impl ProtectivePut { spot_close_fee: Positive, put_open_fee: Positive, put_close_fee: Positive, - ) -> Self { + ) -> Result { let spot_leg = SpotPosition::new( underlying_symbol.clone(), quantity, @@ -123,10 +128,8 @@ impl ProtectivePut { }; strategy.validate(); - strategy - .update_break_even_points() - .expect("Failed to calculate break-even points"); - strategy + strategy.update_break_even_points()?; + Ok(strategy) } /// Returns the spot leg as a Leg enum. @@ -542,6 +545,7 @@ mod tests { pos_or_panic!(0.65), pos_or_panic!(0.65), ) + .unwrap() } #[test] diff --git a/src/strategies/short_butterfly_spread.rs b/src/strategies/short_butterfly_spread.rs index 37a64b42..72ca0c58 100644 --- a/src/strategies/short_butterfly_spread.rs +++ b/src/strategies/short_butterfly_spread.rs @@ -107,6 +107,13 @@ impl ShortButterflySpread { /// # Returns /// /// A fully initialized `ShortButterflySpread` strategy with calculated break-even points. + /// + /// # Errors + /// + /// Returns `StrategyError` if the break-even calculation fails. In + /// practice this branch is unreachable for a freshly-built short + /// butterfly spread and is surfaced only to keep the constructor + /// panic-free. #[allow(clippy::too_many_arguments)] pub fn new( underlying_symbol: String, @@ -128,7 +135,7 @@ impl ShortButterflySpread { close_fee_short_call_low: Positive, open_fee_short_call_high: Positive, close_fee_short_call_high: Positive, - ) -> Self { + ) -> Result { let mut strategy = ShortButterflySpread { name: "Short Butterfly".to_string(), kind: StrategyType::ShortButterflySpread, @@ -216,10 +223,8 @@ impl ShortButterflySpread { strategy.validate(); - strategy - .update_break_even_points() - .expect("Unable to update break even points"); - strategy + strategy.update_break_even_points()?; + Ok(strategy) } } @@ -237,11 +242,12 @@ impl StrategyConstructor for ShortButterflySpread { // Sort options by strike price let mut sorted_positions = vec_positions.to_vec(); + // SAFETY: total order on Positive; f64 fallback to Equal is safe for stable sort sorted_positions.sort_by(|a, b| { a.option .strike_price .partial_cmp(&b.option.strike_price) - .unwrap() + .unwrap_or(std::cmp::Ordering::Equal) }); let lower_strike_position = &sorted_positions[0]; @@ -815,10 +821,10 @@ impl Optimizable for ShortButterflySpread { second: short, third: short_high, }; - let strategy = strategy.create_strategy(option_chain, &legs); - strategy.validate() - && strategy.get_max_profit().is_ok() - && strategy.get_max_loss().is_ok() + match strategy.create_strategy(option_chain, &legs) { + Ok(s) => s.validate() && s.get_max_profit().is_ok() && s.get_max_loss().is_ok(), + Err(_) => false, + } }) // Map to OptionDataGroup .map(move |(short_low, short, short_high)| { @@ -848,11 +854,24 @@ impl Optimizable for ShortButterflySpread { second: short, third: short_high, }; - let strategy = self.create_strategy(option_chain, &legs); + let strategy = match self.create_strategy(option_chain, &legs) { + Ok(s) => s, + Err(e) => { + tracing::warn!(error = %e, "skipping invalid strategy combination"); + continue; + } + }; // Calculate the current value based on the optimization criteria - let current_value = match criteria { - OptimizationCriteria::Ratio => strategy.get_profit_ratio().unwrap(), - OptimizationCriteria::Area => strategy.get_profit_area().unwrap(), + let metric = match criteria { + OptimizationCriteria::Ratio => strategy.get_profit_ratio(), + OptimizationCriteria::Area => strategy.get_profit_area(), + }; + let current_value = match metric { + Ok(v) => v, + Err(e) => { + tracing::warn!(error = %e, "skipping candidate with unscorable metric"); + continue; + } }; if current_value > best_value { @@ -864,7 +883,19 @@ impl Optimizable for ShortButterflySpread { } } - fn create_strategy(&self, chain: &OptionChain, legs: &StrategyLegs) -> Self::Strategy { + /// Constructs a `ShortButterflySpread` from the supplied chain and legs. + /// + /// # Errors + /// + /// Returns `StrategyError::OperationError` when the supplied legs are + /// missing required quotes (`low_strike.call_bid`, + /// `middle_strike.call_ask`, `high_strike.call_bid`) needed to price the + /// strategy. + fn create_strategy( + &self, + chain: &OptionChain, + legs: &StrategyLegs, + ) -> Result { match legs { StrategyLegs::ThreeLegs { first: low_strike, @@ -874,6 +905,25 @@ impl Optimizable for ShortButterflySpread { let implied_volatility = middle_strike.implied_volatility; assert!(implied_volatility <= Positive::ONE); + let low_call_bid = low_strike.call_bid.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing call_bid for low strike leg", + ) + })?; + let middle_call_ask = middle_strike.call_ask.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing call_ask for middle strike leg", + ) + })?; + let high_call_bid = high_strike.call_bid.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing call_bid for high strike leg", + ) + })?; + ShortButterflySpread::new( chain.symbol.clone(), chain.underlying_price, @@ -885,9 +935,9 @@ impl Optimizable for ShortButterflySpread { self.short_call_low.option.risk_free_rate, self.short_call_low.option.dividend_yield, self.short_call_low.option.quantity, - low_strike.call_bid.unwrap(), - middle_strike.call_ask.unwrap(), - high_strike.call_bid.unwrap(), + low_call_bid, + middle_call_ask, + high_call_bid, self.long_call.open_fee, self.long_call.close_fee, self.short_call_low.open_fee, @@ -1090,6 +1140,7 @@ mod tests_short_butterfly_spread { pos_or_panic!(0.05), // open_fee_short_call_high pos_or_panic!(0.05), // close_fee_short_call_high ) + .unwrap() } #[test] @@ -1191,7 +1242,8 @@ mod tests_short_butterfly_spread { pos_or_panic!(0.05), // close_fee_short_call_low Positive::ONE, // open_fee_short_call_high pos_or_panic!(0.05), // close_fee_short_call_high - ); + ) + .unwrap(); assert_eq!(butterfly.short_call_low.open_fee, 1.0); // fees / 3 assert_eq!(butterfly.long_call.open_fee, 1.0); // fees / 3 @@ -1232,7 +1284,8 @@ mod tests_short_butterfly_spread { pos_or_panic!(0.05), // close_fee_short_call_low pos_or_panic!(0.05), // open_fee_short_call_high pos_or_panic!(0.05), // close_fee_short_call_high - ); + ) + .unwrap(); assert_eq!(butterfly.short_call_low.option.quantity, Positive::TWO); assert_eq!(butterfly.long_call.option.quantity, pos_or_panic!(4.0)); // 2 * 2 @@ -1306,7 +1359,8 @@ mod tests_short_butterfly_spread { pos_or_panic!(0.05), // close_fee_short_call_low pos_or_panic!(0.05), // open_fee_short_call_high pos_or_panic!(0.05), // close_fee_short_call_high - ); + ) + .unwrap(); assert!(max_loss.get_max_loss().is_err()); } @@ -1380,7 +1434,8 @@ mod tests_short_butterfly_validation { pos_or_panic!(0.05), // close_fee_short_call_low pos_or_panic!(0.05), // open_fee_short_call_high pos_or_panic!(0.05), // close_fee_short_call_high - ); + ) + .unwrap(); assert!(butterfly.validate()); } @@ -1406,7 +1461,8 @@ mod tests_short_butterfly_validation { pos_or_panic!(0.05), // close_fee_short_call_low pos_or_panic!(0.05), // open_fee_short_call_high pos_or_panic!(0.05), // close_fee_short_call_high - ); + ) + .unwrap(); butterfly.short_call_low = create_valid_position(Side::Short, pos_or_panic!(90.0), Positive::ZERO); assert!(!butterfly.validate()); @@ -1434,7 +1490,8 @@ mod tests_short_butterfly_validation { pos_or_panic!(0.05), // close_fee_short_call_low pos_or_panic!(0.05), // open_fee_short_call_high pos_or_panic!(0.05), // close_fee_short_call_high - ); + ) + .unwrap(); assert!(!butterfly.validate()); } @@ -1460,7 +1517,8 @@ mod tests_short_butterfly_validation { pos_or_panic!(0.05), // close_fee_short_call_low pos_or_panic!(0.05), // open_fee_short_call_high pos_or_panic!(0.05), // close_fee_short_call_high - ); + ) + .unwrap(); butterfly.long_call = create_valid_position(Side::Long, Positive::HUNDRED, Positive::ONE); assert!(!butterfly.validate()); } @@ -1487,7 +1545,8 @@ mod tests_short_butterfly_validation { pos_or_panic!(0.05), // close_fee_short_call_low pos_or_panic!(0.05), // open_fee_short_call_high pos_or_panic!(0.05), // close_fee_short_call_high - ); + ) + .unwrap(); butterfly.short_call_high = create_valid_position(Side::Short, pos_or_panic!(110.0), Positive::TWO); assert!(!butterfly.validate()); @@ -1528,6 +1587,7 @@ mod tests_short_butterfly_profit { pos_or_panic!(0.05), // open_fee_short_call_high pos_or_panic!(0.05), // close_fee_short_call_high ) + .unwrap() } #[test] @@ -1577,7 +1637,8 @@ mod tests_short_butterfly_profit { pos_or_panic!(0.05), // close_fee_short_call_low pos_or_panic!(0.05), // open_fee_short_call_high pos_or_panic!(0.05), // close_fee_short_call_high - ); + ) + .unwrap(); let scaled_profit = butterfly .calculate_profit_at(&pos_or_panic!(85.0)) .unwrap() @@ -1625,7 +1686,8 @@ mod tests_short_butterfly_profit { Positive::ZERO, // close_fee_short_call_low Positive::ZERO, // open_fee_short_call_high Positive::ZERO, // close_fee_short_call_high - ); + ) + .unwrap(); let base_butterfly = create_test(); let profit_without_fees = butterfly @@ -1677,6 +1739,7 @@ mod tests_short_butterfly_delta { pos_or_panic!(0.05), // open_fee_short_call_high pos_or_panic!(0.05), // close_fee_short_call_high ) + .unwrap() } #[test] @@ -1849,6 +1912,7 @@ mod tests_short_butterfly_delta_size { pos_or_panic!(0.05), // open_fee_short_call_high pos_or_panic!(0.05), // close_fee_short_call_high ) + .unwrap() } #[test] @@ -2017,6 +2081,7 @@ mod tests_adjust_option_position_short { pos_or_panic!(0.05), pos_or_panic!(0.05), ) + .unwrap() } #[test] @@ -2141,6 +2206,7 @@ mod tests_short_butterfly_position_management { pos_or_panic!(0.05), pos_or_panic!(0.05), ) + .unwrap() } #[test] @@ -2619,6 +2685,7 @@ mod tests_butterfly_strategies { pos_or_panic!(0.05), // open_fee_short_call_high pos_or_panic!(0.05), // close_fee_short_call_high ) + .unwrap() } #[test] @@ -2705,7 +2772,8 @@ mod tests_butterfly_strategies { Positive::ONE, // close_fee_short_call_low Positive::ONE, // open_fee_short_call_high Positive::ONE, // close_fee_short_call_high - ); + ) + .unwrap(); assert_eq!(butterfly.get_fees().unwrap().to_f64(), 8.0); } @@ -2731,7 +2799,8 @@ mod tests_butterfly_strategies { Positive::ONE, // close_fee_short_call_low Positive::ONE, // open_fee_short_call_high Positive::ONE, // close_fee_short_call_high - ); + ) + .unwrap(); assert_eq!(butterfly.get_fees().unwrap(), pos_or_panic!(16.0)); } @@ -2784,7 +2853,8 @@ mod tests_butterfly_strategies { pos_or_panic!(0.05), pos_or_panic!(0.05), pos_or_panic!(0.05), - ); + ) + .unwrap(); assert_eq!( short_butterfly.get_max_profit().unwrap().to_f64(), pos_or_panic!(18.106) @@ -2851,6 +2921,7 @@ mod tests_butterfly_optimizable { pos_or_panic!(0.05), // open_fee_short_call_high pos_or_panic!(0.05), // close_fee_short_call_high ) + .unwrap() } #[test] @@ -2972,6 +3043,7 @@ mod tests_butterfly_probability { pos_or_panic!(0.05), // open_fee_short_call_high pos_or_panic!(0.05), // close_fee_short_call_high ) + .unwrap() } mod short_butterfly_tests { @@ -3015,7 +3087,8 @@ mod tests_butterfly_probability { pos_or_panic!(0.05), pos_or_panic!(0.05), pos_or_panic!(0.05), - ); + ) + .unwrap(); let ranges = butterfly.get_profit_ranges().unwrap(); assert!(ranges[0].upper_bound.is_some()); diff --git a/src/strategies/short_call.rs b/src/strategies/short_call.rs index 7cab34a5..97dc2f9e 100644 --- a/src/strategies/short_call.rs +++ b/src/strategies/short_call.rs @@ -99,10 +99,11 @@ impl ShortCall { /// Returns an initialized `ShortCall` strategy instance. The instance includes the short call /// option position with the specified parameters. /// - /// # Panics - /// - /// This function will panic if the short call option created using the specified parameters - /// fails to meet validity requirements during the `add_position` operation. + /// # Errors + /// Returns `StrategyError` if the freshly-constructed short call leg + /// cannot be added to the strategy. In practice this branch is + /// unreachable for a freshly-built single-leg strategy and is surfaced + /// only to keep the constructor panic-free. /// #[allow(clippy::too_many_arguments, dead_code)] fn new( @@ -117,7 +118,7 @@ impl ShortCall { premium_short_call: Positive, open_fee_short_call: Positive, close_fee_short_call: Positive, - ) -> Self { + ) -> Result { let mut strategy = ShortCall::default(); let short_call_option = Options::new( @@ -143,11 +144,9 @@ impl ShortCall { None, None, ); - strategy - .add_position(&short_call) - .expect("Invalid short call option"); + strategy.add_position(&short_call)?; - strategy + Ok(strategy) } } @@ -570,7 +569,9 @@ where .template( "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} simulations ({eta})", ) - .expect("Failed to set progress bar template") + .map_err(|e| SimulationError::OtherError { + reason: format!("Failed to set progress bar template: {}", e), + })? .progress_chars("#>-"), ); @@ -659,7 +660,7 @@ where // Calculate expiration premium let mut exp_option = self.short_call.option.clone(); exp_option.underlying_price = final_price; - exp_option.expiration_date = ExpirationDate::Days(Positive::new(0.001).unwrap()); + exp_option.expiration_date = ExpirationDate::Days(Positive::new(0.001)?); expiration_premium = Some(exp_option.calculate_price_black_scholes()?.abs()); expired = true; @@ -801,6 +802,7 @@ mod tests_simulate { Positive::ZERO, Positive::ZERO, ) + .unwrap() } fn create_walk_params(prices: Vec) -> WalkParams { diff --git a/src/strategies/short_put.rs b/src/strategies/short_put.rs index dc79510e..a5c163f6 100644 --- a/src/strategies/short_put.rs +++ b/src/strategies/short_put.rs @@ -102,10 +102,11 @@ impl ShortPut { /// /// A new instance of `ShortPut` containing the initialized short put position. /// - /// # Panics - /// - /// This function will panic if adding the short put position to the strategy fails, - /// which may happen if the position is deemed invalid. + /// # Errors + /// Returns `StrategyError` if the freshly-constructed short put leg + /// cannot be added to the strategy. In practice this branch is + /// unreachable for a freshly-built single-leg strategy and is surfaced + /// only to keep the constructor panic-free. /// #[allow(clippy::too_many_arguments, dead_code)] pub fn new( @@ -120,7 +121,7 @@ impl ShortPut { premium_short_put: Positive, open_fee_short_put: Positive, close_fee_short_put: Positive, - ) -> Self { + ) -> Result { let mut strategy = ShortPut::default(); let short_put_option = Options::new( @@ -146,11 +147,9 @@ impl ShortPut { None, None, ); - strategy - .add_position(&short_put) - .expect("Invalid short put option"); + strategy.add_position(&short_put)?; - strategy + Ok(strategy) } } @@ -566,7 +565,9 @@ where .template( "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} simulations ({eta})", ) - .expect("Failed to set progress bar template") + .map_err(|e| SimulationError::OtherError { + reason: format!("Failed to set progress bar template: {}", e), + })? .progress_chars("#>-"), ); @@ -656,7 +657,7 @@ where // Calculate expiration premium (use very small time instead of zero) let mut exp_option = self.short_put.option.clone(); exp_option.underlying_price = final_price; - exp_option.expiration_date = ExpirationDate::Days(Positive::new(0.001).unwrap()); + exp_option.expiration_date = ExpirationDate::Days(Positive::new(0.001)?); expiration_premium = Some(exp_option.calculate_price_black_scholes()?.abs()); expired = true; @@ -801,6 +802,7 @@ mod tests_simulate { Positive::ZERO, // open fee Positive::ZERO, // close fee ) + .unwrap() } /// Helper to create WalkParams with Historical data diff --git a/src/strategies/short_straddle.rs b/src/strategies/short_straddle.rs index a20d1d01..1bf66042 100644 --- a/src/strategies/short_straddle.rs +++ b/src/strategies/short_straddle.rs @@ -162,6 +162,12 @@ impl ShortStraddle { /// - Calculated break-even points /// - Strategy metadata (name, description, etc.) /// + /// # Errors + /// + /// Returns `StrategyError` if either freshly-constructed leg cannot be + /// added to the strategy or if the break-even calculation fails. In + /// practice these branches are unreachable for a freshly-built short + /// straddle and are surfaced only to keep the constructor panic-free. #[allow(clippy::too_many_arguments)] pub fn new( underlying_symbol: String, @@ -178,7 +184,7 @@ impl ShortStraddle { close_fee_short_call: Positive, open_fee_short_put: Positive, close_fee_short_put: Positive, - ) -> Self { + ) -> Result { if strike == Positive::ZERO { strike = underlying_price; } @@ -215,9 +221,7 @@ impl ShortStraddle { None, None, ); - strategy - .add_position(&short_call) - .expect("Invalid short call"); + strategy.add_position(&short_call)?; let short_put_option = Options::new( OptionType::European, @@ -242,14 +246,10 @@ impl ShortStraddle { None, None, ); - strategy - .add_position(&short_put) - .expect("Invalid short put"); + strategy.add_position(&short_put)?; - strategy - .update_break_even_points() - .expect("Unable to update break even points"); - strategy + strategy.update_break_even_points()?; + Ok(strategy) } } @@ -643,13 +643,13 @@ impl Strategies for ShortStraddle { let strike_diff = self.break_even_points[1] - self.break_even_points[0]; let cat = (strike_diff / 2.0_f64.sqrt()).to_f64(); let result = (cat.powf(2.0)) / (2.0 * 10.0_f64.powf(cat.log10().ceil())); - Ok(Decimal::from_f64(result).unwrap()) + Decimal::from_f64(result).ok_or_else(|| StrategyError::numeric_conversion(result)) } fn get_profit_ratio(&self) -> Result { let break_even_diff = self.break_even_points[1] - self.break_even_points[0]; let result = self.get_max_profit().unwrap_or(Positive::ZERO).to_f64() / break_even_diff * 100.0; - Ok(Decimal::from_f64(result).unwrap()) + Decimal::from_f64(result).ok_or_else(|| StrategyError::numeric_conversion(result)) } } @@ -698,10 +698,10 @@ impl Optimizable for ShortStraddle { first: both, second: both, }; - let strategy = strategy.create_strategy(option_chain, &legs); - strategy.validate() - && strategy.get_max_profit().is_ok() - && strategy.get_max_loss().is_ok() + match strategy.create_strategy(option_chain, &legs) { + Ok(s) => s.validate() && s.get_max_profit().is_ok() && s.get_max_loss().is_ok(), + Err(_) => false, + } }) // Map to OptionDataGroup .map(OptionDataGroup::One) @@ -728,11 +728,24 @@ impl Optimizable for ShortStraddle { first: both, second: both, }; - let strategy = self.create_strategy(option_chain, &legs); + let strategy = match self.create_strategy(option_chain, &legs) { + Ok(s) => s, + Err(e) => { + tracing::warn!(error = %e, "skipping invalid strategy combination"); + continue; + } + }; // Calculate the current value based on the optimization criteria - let current_value = match criteria { - OptimizationCriteria::Ratio => strategy.get_profit_ratio().unwrap(), - OptimizationCriteria::Area => strategy.get_profit_area().unwrap(), + let metric = match criteria { + OptimizationCriteria::Ratio => strategy.get_profit_ratio(), + OptimizationCriteria::Area => strategy.get_profit_area(), + }; + let current_value = match metric { + Ok(v) => v, + Err(e) => { + tracing::warn!(error = %e, "skipping candidate with unscorable metric"); + continue; + } }; if current_value > best_value { @@ -744,7 +757,18 @@ impl Optimizable for ShortStraddle { } } - fn create_strategy(&self, chain: &OptionChain, legs: &StrategyLegs) -> Self::Strategy { + /// Constructs a `ShortStraddle` from the supplied chain and legs. + /// + /// # Errors + /// + /// Returns `StrategyError::OperationError` when the supplied legs are + /// missing required quotes (`call.call_bid`, `put.put_bid`) needed to + /// price the strategy. + fn create_strategy( + &self, + chain: &OptionChain, + legs: &StrategyLegs, + ) -> Result { let (call, put) = match legs { StrategyLegs::TwoLegs { first, second } => (first, second), _ => panic!("Invalid number of legs for this strategy"), @@ -760,6 +784,18 @@ impl Optimizable for ShortStraddle { let implied_volatility = call.implied_volatility; assert!(implied_volatility <= Positive::ONE); + let call_bid = call.call_bid.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing call_bid for short call leg", + ) + })?; + let put_bid = put.put_bid.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing put_bid for short put leg", + ) + })?; ShortStraddle::new( chain.symbol.clone(), chain.underlying_price, @@ -769,8 +805,8 @@ impl Optimizable for ShortStraddle { self.short_call.option.risk_free_rate, self.short_call.option.dividend_yield, self.short_call.option.quantity, - call.call_bid.unwrap(), - put.put_bid.unwrap(), + call_bid, + put_bid, self.short_call.open_fee, self.short_call.close_fee, self.short_put.open_fee, @@ -955,6 +991,7 @@ mod tests_short_straddle { pos_or_panic!(0.1), pos_or_panic!(0.1), ) + .unwrap() } #[test] @@ -975,7 +1012,8 @@ mod tests_short_straddle { pos_or_panic!(0.1), pos_or_panic!(0.1), pos_or_panic!(0.1), - ); + ) + .unwrap(); assert_eq!( strategy.short_call.option.strike_price, underlying_price, @@ -1029,7 +1067,8 @@ mod tests_short_straddle { pos_or_panic!(0.1), pos_or_panic!(0.1), pos_or_panic!(0.1), - ); + ) + .unwrap(); assert!(valid_strategy.validate()); assert_eq!( valid_strategy.short_call.option.strike_price, @@ -1207,7 +1246,7 @@ mod tests_short_straddle { first: call_option, second: put_option, }; - let new_strategy = strategy.create_strategy(&chain, &legs); + let new_strategy = strategy.create_strategy(&chain, &legs).unwrap(); assert!(new_strategy.validate()); } @@ -1264,6 +1303,7 @@ mod tests_short_straddle_probability { Positive::ZERO, // open_fee_short_put Positive::ZERO, // close_fee_short_put ) + .unwrap() } #[test] @@ -1399,6 +1439,7 @@ mod tests_short_straddle_probability_bis { Positive::ZERO, // open_fee_short_put Positive::ZERO, // close_fee_short_put ) + .unwrap() } #[test] @@ -1536,6 +1577,7 @@ mod tests_short_straddle_delta { pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put ) + .unwrap() } #[test] @@ -1679,6 +1721,7 @@ mod tests_short_straddle_delta_size { pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put ) + .unwrap() } #[test] @@ -2116,6 +2159,7 @@ mod tests_straddle_position_management { pos_or_panic!(0.1), // open_fee_short_put pos_or_panic!(0.1), // close_fee_short_put ) + .unwrap() } #[test] @@ -2226,6 +2270,7 @@ mod tests_adjust_option_position { pos_or_panic!(0.1), // open_fee_short_put pos_or_panic!(0.1), // close_fee_short_put ) + .unwrap() } #[test] diff --git a/src/strategies/short_strangle.rs b/src/strategies/short_strangle.rs index 2bdaa3a6..1d9027fa 100644 --- a/src/strategies/short_strangle.rs +++ b/src/strategies/short_strangle.rs @@ -146,6 +146,13 @@ impl ShortStrangle { /// The strategy has two break-even points: /// - Lower break-even: Put strike minus the total premium received per contract /// - Upper break-even: Call strike plus the total premium received per contract + /// + /// # Errors + /// + /// Returns `StrategyError` if either freshly-constructed leg cannot be + /// added to the strategy or if the break-even calculation fails. In + /// practice these branches are unreachable for a freshly-built + /// strangle and are surfaced only to keep the constructor panic-free. #[allow(clippy::too_many_arguments)] pub fn new( underlying_symbol: String, @@ -164,7 +171,7 @@ impl ShortStrangle { close_fee_short_call: Positive, open_fee_short_put: Positive, close_fee_short_put: Positive, - ) -> Self { + ) -> Result { if call_strike == Positive::ZERO { call_strike = underlying_price * 1.1; } @@ -203,9 +210,7 @@ impl ShortStrangle { None, None, ); - strategy - .add_position(&short_call) - .expect("Invalid position"); + strategy.add_position(&short_call)?; let short_put_option = Options::new( OptionType::European, @@ -230,12 +235,10 @@ impl ShortStrangle { None, None, ); - strategy.add_position(&short_put).expect("Invalid position"); + strategy.add_position(&short_put)?; - strategy - .update_break_even_points() - .expect("Unable to update break even points"); - strategy + strategy.update_break_even_points()?; + Ok(strategy) } } @@ -253,11 +256,12 @@ impl StrategyConstructor for ShortStrangle { // Sort options by option style to identify call and put let mut sorted_positions = vec_positions.to_vec(); + // SAFETY: total order on Positive; f64 fallback to Equal is safe for stable sort sorted_positions.sort_by(|a, b| { a.option .strike_price .partial_cmp(&b.option.strike_price) - .unwrap() + .unwrap_or(std::cmp::Ordering::Equal) }); let put_position = &sorted_positions[0]; // Put will be first @@ -683,7 +687,7 @@ impl Strategies for ShortStrangle { } fn get_max_profit(&self) -> Result { - let max_profit = self.get_net_premium_received().unwrap().to_f64(); + let max_profit = self.get_net_premium_received()?.to_f64(); if max_profit < ZERO { Err(StrategyError::ProfitLossError( ProfitLossErrorKind::MaxProfitError { @@ -710,7 +714,7 @@ impl Strategies for ShortStrangle { let outer_square = break_even_diff * max_profit; let triangles = (outer_square - inner_square) / 2.0; let result = ((inner_square + triangles) / self.one_option().underlying_price).to_f64(); - Ok(Decimal::from_f64(result).unwrap()) + Decimal::from_f64(result).ok_or_else(|| StrategyError::numeric_conversion(result)) } fn get_profit_ratio(&self) -> Result { @@ -719,7 +723,7 @@ impl Strategies for ShortStrangle { Ok(max_profit) => max_profit.to_f64() / break_even_diff * 100.0, Err(_) => ZERO, }; - Ok(Decimal::from_f64(result).unwrap()) + Decimal::from_f64(result).ok_or_else(|| StrategyError::numeric_conversion(result)) } fn get_best_range_to_show(&self, step: Positive) -> Result, StrategyError> { @@ -880,12 +884,19 @@ impl Optimizable for ShortStrangle { let (_, delta_put) = short_put.current_deltas(); let (delta_call, _) = short_call.current_deltas(); - let is_valid = delta_put.unwrap() >= -delta.to_dec() - && delta_call.unwrap() <= delta.to_dec() - && delta_put.unwrap().is_sign_negative() - && delta_call.unwrap().is_sign_positive() - && !delta_call.unwrap().is_zero() - && !delta_put.unwrap().is_zero(); + let (Some(dp), Some(dc)) = (delta_put, delta_call) else { + trace!( + "Missing delta on PUT {:?} or CALL {:?}", + delta_put, delta_call + ); + return false; + }; + let is_valid = dp >= -delta.to_dec() + && dc <= delta.to_dec() + && dp.is_sign_negative() + && dc.is_sign_positive() + && !dc.is_zero() + && !dp.is_zero(); if !is_valid { trace!( "Not Valid Delta combination: PUT {:?} and CALL {:?}", @@ -897,11 +908,11 @@ impl Optimizable for ShortStrangle { FindOptimalSide::DeltaRange(min, max) => { let (_, delta_put) = short_put.current_deltas(); let (delta_call, _) = short_call.current_deltas(); - let delta_put_positive = delta_put.unwrap().abs(); - delta_put_positive > min - && delta_put_positive < max - && delta_call.unwrap() > min - && delta_call.unwrap() < max + let (Some(dp), Some(dc)) = (delta_put, delta_call) else { + return false; + }; + let delta_put_positive = dp.abs(); + delta_put_positive > min && delta_put_positive < max && dc > min && dc < max } FindOptimalSide::Center => { short_put.is_valid_optimal_side(underlying_price, &FindOptimalSide::Lower) @@ -926,10 +937,10 @@ impl Optimizable for ShortStrangle { second: short_call, }; trace!("Legs: {:?}", legs); - let strategy = strategy.create_strategy(option_chain, &legs); - strategy.validate() - && strategy.get_max_profit().is_ok() - && strategy.get_max_loss().is_ok() + match strategy.create_strategy(option_chain, &legs) { + Ok(s) => s.validate() && s.get_max_profit().is_ok() && s.get_max_loss().is_ok(), + Err(_) => false, + } }) // Map to OptionDataGroup .map(move |(short_put, short_call)| OptionDataGroup::Two(short_put, short_call)) @@ -969,11 +980,24 @@ impl Optimizable for ShortStrangle { first: short_put, second: short_call, }; - let strategy = self.create_strategy(option_chain, &legs); + let strategy = match self.create_strategy(option_chain, &legs) { + Ok(s) => s, + Err(e) => { + warn!(error = %e, "skipping invalid strategy combination"); + continue; + } + }; // Calculate the current value based on the optimization criteria - let current_value = match criteria { - OptimizationCriteria::Ratio => strategy.get_profit_ratio().unwrap(), - OptimizationCriteria::Area => strategy.get_profit_area().unwrap(), + let metric = match criteria { + OptimizationCriteria::Ratio => strategy.get_profit_ratio(), + OptimizationCriteria::Area => strategy.get_profit_area(), + }; + let current_value = match metric { + Ok(v) => v, + Err(e) => { + warn!(error = %e, "skipping candidate with unscorable metric"); + continue; + } }; if current_value > best_value { @@ -994,7 +1018,18 @@ impl Optimizable for ShortStrangle { && short_call.call_ask.unwrap_or(Positive::ZERO) > Positive::ZERO } - fn create_strategy(&self, chain: &OptionChain, legs: &StrategyLegs) -> Self::Strategy { + /// Constructs a `ShortStrangle` from the supplied chain and legs. + /// + /// # Errors + /// + /// Returns `StrategyError::OperationError` when the supplied legs are + /// missing required quotes (`call.call_bid`, `put.put_bid`) needed to + /// price the strategy. + fn create_strategy( + &self, + chain: &OptionChain, + legs: &StrategyLegs, + ) -> Result { let (put, call) = match legs { StrategyLegs::TwoLegs { first, second } => (first, second), _ => panic!("Invalid number of legs for this strategy"), @@ -1019,6 +1054,19 @@ impl Optimizable for ShortStrangle { self.one_option().expiration_date }; + let call_bid = call.call_bid.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing call_bid for short call leg", + ) + })?; + let put_bid = put.put_bid.ok_or_else(|| { + StrategyError::operation_not_supported( + "create_strategy", + "missing put_bid for short put leg", + ) + })?; + ShortStrangle::new( chain.symbol.clone(), chain.underlying_price, @@ -1030,8 +1078,8 @@ impl Optimizable for ShortStrangle { self.one_option().risk_free_rate, self.one_option().dividend_yield, self.one_option().quantity, - call.call_bid.unwrap(), - put.put_bid.unwrap(), + call_bid, + put_bid, self.short_call.open_fee, self.short_call.close_fee, self.short_put.open_fee, @@ -1207,8 +1255,8 @@ impl PnLCalculator for ShortStrangle { PnL { realized: None, unrealized: None, - initial_costs: position.total_cost().unwrap(), - initial_income: position.premium_received().unwrap(), + initial_costs: position.total_cost()?, + initial_income: position.premium_received()?, date_time: Utc::now(), } } @@ -1221,8 +1269,8 @@ impl PnLCalculator for ShortStrangle { PnL { realized: None, unrealized: None, - initial_costs: position.total_cost().unwrap(), - initial_income: position.premium_received().unwrap(), + initial_costs: position.total_cost()?, + initial_income: position.premium_received()?, date_time: Utc::now(), } } @@ -1241,15 +1289,15 @@ impl PnLCalculator for ShortStrangle { match (side, option_style) { (Side::Short, OptionStyle::Call) => { let mut position = self.short_call.clone(); - position.option.side = Side::Long; // Sell the short call + position.option.side = Side::Long; // Sell the short call position.option.quantity = *quantity; position.option.strike_price = *strike; PnL { realized: None, unrealized: None, - initial_costs: position.total_cost().unwrap(), - initial_income: position.premium_received().unwrap(), + initial_costs: position.total_cost()?, + initial_income: position.premium_received()?, date_time: Utc::now(), } } @@ -1262,8 +1310,8 @@ impl PnLCalculator for ShortStrangle { PnL { realized: None, unrealized: None, - initial_costs: position.total_cost().unwrap(), - initial_income: position.premium_received().unwrap(), + initial_costs: position.total_cost()?, + initial_income: position.premium_received()?, date_time: Utc::now(), } } @@ -1314,6 +1362,7 @@ mod tests_short_strangle { pos_or_panic!(0.1), pos_or_panic!(0.1), ) + .unwrap() } #[test] @@ -1501,7 +1550,7 @@ is expected and the underlying asset's price is anticipated to remain stable." second: call_option, }; - let new_strategy = strategy.create_strategy(&chain, &legs); + let new_strategy = strategy.create_strategy(&chain, &legs).unwrap(); assert!(new_strategy.validate()); } fn create_test_option_chain() -> OptionChain { @@ -1559,6 +1608,7 @@ mod tests_short_strangle_probability { Positive::ZERO, // open_fee_short_put Positive::ZERO, // close_fee_short_put ) + .unwrap() } #[test] @@ -1696,6 +1746,7 @@ mod tests_short_strangle_probability_bis { Positive::ZERO, // open_fee_short_put Positive::ZERO, // close_fee_short_put ) + .unwrap() } #[test] @@ -1835,6 +1886,7 @@ mod tests_short_strangle_delta { pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put ) + .unwrap() } #[test] @@ -2024,6 +2076,7 @@ mod tests_short_strangle_delta_size { pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put ) + .unwrap() } #[test] @@ -2358,6 +2411,7 @@ mod tests_adjust_option_position_short { pos_or_panic!(0.1), // open_fee_short_put pos_or_panic!(0.1), // close_fee_short_put ) + .unwrap() } #[test] @@ -3187,6 +3241,7 @@ mod test_adjustments_pnl { pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put ) + .unwrap() } #[test] @@ -3327,6 +3382,7 @@ mod test_valid_premium_for_shorts { pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put ) + .unwrap() } #[test] @@ -3367,6 +3423,7 @@ mod tests_strangle_position_management { pos_or_panic!(0.1), // open_fee_short_put pos_or_panic!(0.1), // close_fee_short_put ) + .unwrap() } #[test] @@ -3478,6 +3535,7 @@ mod tests_generate_delta_adjustments { pos_or_panic!(0.1), // open_fee_short_put pos_or_panic!(0.1), // close_fee_short_put ) + .unwrap() } #[test] diff --git a/tests/unit/strategies/custom_test.rs b/tests/unit/strategies/custom_test.rs index 9289b5f7..b4968e25 100644 --- a/tests/unit/strategies/custom_test.rs +++ b/tests/unit/strategies/custom_test.rs @@ -84,6 +84,7 @@ fn create_test_custom_strategy() -> CustomStrategy { 30, // Days to expiration Positive::new(0.25).unwrap(), // Implied volatility ) + .unwrap() } // Helper function to create a complex Custom Strategy for testing @@ -208,6 +209,7 @@ fn create_complex_custom_strategy() -> CustomStrategy { 45, Positive::new(0.21).unwrap(), ) + .unwrap() } #[cfg(test)] diff --git a/tests/unit/strategies/delta/strategy_bear_call_spread.rs b/tests/unit/strategies/delta/strategy_bear_call_spread.rs index d239c394..ab2c8947 100644 --- a/tests/unit/strategies/delta/strategy_bear_call_spread.rs +++ b/tests/unit/strategies/delta/strategy_bear_call_spread.rs @@ -30,7 +30,7 @@ fn test_bear_call_spread_integration() -> Result<(), Box> { pos_or_panic!(0.78), // open_fee_long pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short - ); + )?; let greeks = strategy.greeks().unwrap(); let epsilon = dec!(0.001); diff --git a/tests/unit/strategies/delta/strategy_bear_put_spread.rs b/tests/unit/strategies/delta/strategy_bear_put_spread.rs index 0b7586b8..71853aff 100644 --- a/tests/unit/strategies/delta/strategy_bear_put_spread.rs +++ b/tests/unit/strategies/delta/strategy_bear_put_spread.rs @@ -30,7 +30,7 @@ fn test_bear_put_spread_integration() -> Result<(), Box> { pos_or_panic!(0.78), // open_fee_long pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short - ); + )?; let greeks = strategy.greeks().unwrap(); let epsilon = dec!(0.001); diff --git a/tests/unit/strategies/delta/strategy_bull_call_spread.rs b/tests/unit/strategies/delta/strategy_bull_call_spread.rs index 4fd84f8d..a5b7cba1 100644 --- a/tests/unit/strategies/delta/strategy_bull_call_spread.rs +++ b/tests/unit/strategies/delta/strategy_bull_call_spread.rs @@ -30,7 +30,7 @@ fn test_bull_call_spread_integration() -> Result<(), Box> { pos_or_panic!(0.78), // open_fee_long pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short - ); + )?; let greeks = strategy.greeks().unwrap(); let epsilon = dec!(0.001); diff --git a/tests/unit/strategies/delta/strategy_bull_put_spread.rs b/tests/unit/strategies/delta/strategy_bull_put_spread.rs index ee4e265e..b899fa24 100644 --- a/tests/unit/strategies/delta/strategy_bull_put_spread.rs +++ b/tests/unit/strategies/delta/strategy_bull_put_spread.rs @@ -30,7 +30,7 @@ fn test_bull_put_spread_integration() -> Result<(), Box> { pos_or_panic!(0.78), // open_fee_long pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short - ); + )?; let greeks = strategy.greeks().unwrap(); let epsilon = dec!(0.001); diff --git a/tests/unit/strategies/delta/strategy_call_butterfly.rs b/tests/unit/strategies/delta/strategy_call_butterfly.rs index 52726cb2..02627e93 100644 --- a/tests/unit/strategies/delta/strategy_call_butterfly.rs +++ b/tests/unit/strategies/delta/strategy_call_butterfly.rs @@ -34,7 +34,7 @@ fn test_call_butterfly_integration() -> Result<(), Box> { pos_or_panic!(0.73), // close_fee_short pos_or_panic!(0.73), // close_fee_short pos_or_panic!(0.73), // open_fee_short - ); + )?; let greeks = strategy.greeks().unwrap(); let epsilon = dec!(0.001); diff --git a/tests/unit/strategies/delta/strategy_iron_butterfly.rs b/tests/unit/strategies/delta/strategy_iron_butterfly.rs index f720a942..20613bdb 100644 --- a/tests/unit/strategies/delta/strategy_iron_butterfly.rs +++ b/tests/unit/strategies/delta/strategy_iron_butterfly.rs @@ -31,7 +31,7 @@ fn test_iron_butterfly_integration() -> Result<(), Box> { pos_or_panic!(16.8), // premium_long_put pos_or_panic!(0.96), // open_fee pos_or_panic!(0.96), // close_fee - ); + )?; let greeks = strategy.greeks().unwrap(); let epsilon = dec!(0.001); diff --git a/tests/unit/strategies/delta/strategy_iron_condor.rs b/tests/unit/strategies/delta/strategy_iron_condor.rs index 0db40ef3..396fddcb 100644 --- a/tests/unit/strategies/delta/strategy_iron_condor.rs +++ b/tests/unit/strategies/delta/strategy_iron_condor.rs @@ -32,7 +32,7 @@ fn test_iron_condor_integration() -> Result<(), Box> { pos_or_panic!(16.8), // premium_long_put pos_or_panic!(0.96), // open_fee pos_or_panic!(0.96), // close_fee - ); + )?; let greeks = strategy.greeks().unwrap(); let epsilon = dec!(0.001); diff --git a/tests/unit/strategies/delta/strategy_long_butterfly_spread.rs b/tests/unit/strategies/delta/strategy_long_butterfly_spread.rs index 4a8c7d51..6cbb28d9 100644 --- a/tests/unit/strategies/delta/strategy_long_butterfly_spread.rs +++ b/tests/unit/strategies/delta/strategy_long_butterfly_spread.rs @@ -34,7 +34,7 @@ fn test_long_butterfly_spread_integration() -> Result<(), Box> { pos_or_panic!(0.07), // fees pos_or_panic!(0.05), // fees pos_or_panic!(0.03), // fees - ); + )?; let greeks = strategy.greeks().unwrap(); let epsilon = dec!(0.001); diff --git a/tests/unit/strategies/delta/strategy_long_straddle.rs b/tests/unit/strategies/delta/strategy_long_straddle.rs index 2a5cd92d..b56d9e4c 100644 --- a/tests/unit/strategies/delta/strategy_long_straddle.rs +++ b/tests/unit/strategies/delta/strategy_long_straddle.rs @@ -29,7 +29,7 @@ fn test_long_straddle_integration() -> Result<(), Box> { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + )?; let greeks = strategy.greeks().unwrap(); let epsilon = dec!(0.001); diff --git a/tests/unit/strategies/delta/strategy_long_strangle.rs b/tests/unit/strategies/delta/strategy_long_strangle.rs index 94d791c0..82c9cfb7 100644 --- a/tests/unit/strategies/delta/strategy_long_strangle.rs +++ b/tests/unit/strategies/delta/strategy_long_strangle.rs @@ -30,7 +30,7 @@ fn test_long_strangle_integration() -> Result<(), Box> { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + )?; let greeks = strategy.greeks().unwrap(); let epsilon = DELTA_THRESHOLD; diff --git a/tests/unit/strategies/delta/strategy_poor_mans_covered_call.rs b/tests/unit/strategies/delta/strategy_poor_mans_covered_call.rs index a8b44635..7f012a59 100644 --- a/tests/unit/strategies/delta/strategy_poor_mans_covered_call.rs +++ b/tests/unit/strategies/delta/strategy_poor_mans_covered_call.rs @@ -30,7 +30,7 @@ fn test_poor_mans_covered_call_integration() -> Result<(), Box> { pos_or_panic!(1.74), // close_fee_short_call pos_or_panic!(0.85), // open_fee_short_put pos_or_panic!(0.85), // close_fee_short_put - ); + )?; let greeks = strategy.greeks().unwrap(); let epsilon = dec!(0.001); diff --git a/tests/unit/strategies/delta/strategy_short_butterfly_spread.rs b/tests/unit/strategies/delta/strategy_short_butterfly_spread.rs index 51a3dfab..a3f6e72f 100644 --- a/tests/unit/strategies/delta/strategy_short_butterfly_spread.rs +++ b/tests/unit/strategies/delta/strategy_short_butterfly_spread.rs @@ -32,7 +32,7 @@ fn test_short_butterfly_spread_integration() -> Result<(), Box> { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + )?; let greeks = strategy.greeks().unwrap(); let epsilon = dec!(0.001); diff --git a/tests/unit/strategies/delta/strategy_short_straddle.rs b/tests/unit/strategies/delta/strategy_short_straddle.rs index 3b14e17b..910ca64b 100644 --- a/tests/unit/strategies/delta/strategy_short_straddle.rs +++ b/tests/unit/strategies/delta/strategy_short_straddle.rs @@ -28,7 +28,7 @@ fn test_short_straddle_integration() -> Result<(), Box> { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + )?; let greeks = strategy.greeks().unwrap(); let epsilon = dec!(0.001); diff --git a/tests/unit/strategies/delta/strategy_short_strangle.rs b/tests/unit/strategies/delta/strategy_short_strangle.rs index 85a47aa5..c49216d7 100644 --- a/tests/unit/strategies/delta/strategy_short_strangle.rs +++ b/tests/unit/strategies/delta/strategy_short_strangle.rs @@ -31,7 +31,7 @@ fn test_short_strangle_with_greeks_integration() -> Result<(), Box> { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + )?; let greeks = strategy.greeks().unwrap(); let epsilon = DELTA_THRESHOLD; diff --git a/tests/unit/strategies/optimal/strategy_bear_call_spread.rs b/tests/unit/strategies/optimal/strategy_bear_call_spread.rs index dd56cc3b..99b07033 100644 --- a/tests/unit/strategies/optimal/strategy_bear_call_spread.rs +++ b/tests/unit/strategies/optimal/strategy_bear_call_spread.rs @@ -30,7 +30,7 @@ fn test_bear_call_spread_integration() -> Result<(), Box> { pos_or_panic!(0.78), // open_fee_long pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal/strategy_bear_put_spread.rs b/tests/unit/strategies/optimal/strategy_bear_put_spread.rs index a219d42c..3209c4a3 100644 --- a/tests/unit/strategies/optimal/strategy_bear_put_spread.rs +++ b/tests/unit/strategies/optimal/strategy_bear_put_spread.rs @@ -31,7 +31,7 @@ fn test_bear_put_spread_integration() -> Result<(), Box> { pos_or_panic!(0.78), // open_fee_long pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal/strategy_bull_call_spread.rs b/tests/unit/strategies/optimal/strategy_bull_call_spread.rs index 0cb2174e..119c0258 100644 --- a/tests/unit/strategies/optimal/strategy_bull_call_spread.rs +++ b/tests/unit/strategies/optimal/strategy_bull_call_spread.rs @@ -31,7 +31,7 @@ fn test_bull_call_spread_integration() -> Result<(), Box> { pos_or_panic!(0.78), // open_fee_long pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal/strategy_bull_put_spread.rs b/tests/unit/strategies/optimal/strategy_bull_put_spread.rs index a8b0108e..9c812414 100644 --- a/tests/unit/strategies/optimal/strategy_bull_put_spread.rs +++ b/tests/unit/strategies/optimal/strategy_bull_put_spread.rs @@ -31,7 +31,7 @@ fn test_bull_put_spread_integration() -> Result<(), Box> { pos_or_panic!(0.78), // open_fee_long pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal/strategy_call_butterfly.rs b/tests/unit/strategies/optimal/strategy_call_butterfly.rs index 1ddeb341..28439e29 100644 --- a/tests/unit/strategies/optimal/strategy_call_butterfly.rs +++ b/tests/unit/strategies/optimal/strategy_call_butterfly.rs @@ -35,7 +35,7 @@ fn test_call_butterfly_integration() -> Result<(), Box> { pos_or_panic!(0.73), // close_fee_short pos_or_panic!(0.73), // close_fee_short pos_or_panic!(0.73), // open_fee_short - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal/strategy_iron_butterfly.rs b/tests/unit/strategies/optimal/strategy_iron_butterfly.rs index 24011c25..162ad7e0 100644 --- a/tests/unit/strategies/optimal/strategy_iron_butterfly.rs +++ b/tests/unit/strategies/optimal/strategy_iron_butterfly.rs @@ -32,7 +32,7 @@ fn test_iron_butterfly_integration() -> Result<(), Box> { pos_or_panic!(16.8), // premium_long_put pos_or_panic!(0.96), // open_fee pos_or_panic!(0.96), // close_fee - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal/strategy_iron_condor.rs b/tests/unit/strategies/optimal/strategy_iron_condor.rs index ed240588..03c9cfaf 100644 --- a/tests/unit/strategies/optimal/strategy_iron_condor.rs +++ b/tests/unit/strategies/optimal/strategy_iron_condor.rs @@ -33,7 +33,7 @@ fn test_iron_condor_integration() -> Result<(), Box> { pos_or_panic!(16.8), // premium_long_put pos_or_panic!(0.96), // open_fee pos_or_panic!(0.96), // close_fee - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal/strategy_long_butterfly_spread.rs b/tests/unit/strategies/optimal/strategy_long_butterfly_spread.rs index 0f6b95b3..027b2841 100644 --- a/tests/unit/strategies/optimal/strategy_long_butterfly_spread.rs +++ b/tests/unit/strategies/optimal/strategy_long_butterfly_spread.rs @@ -35,7 +35,7 @@ fn test_long_butterfly_spread_integration() -> Result<(), Box> { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal/strategy_long_straddle.rs b/tests/unit/strategies/optimal/strategy_long_straddle.rs index ed65fd42..6333377a 100644 --- a/tests/unit/strategies/optimal/strategy_long_straddle.rs +++ b/tests/unit/strategies/optimal/strategy_long_straddle.rs @@ -30,7 +30,7 @@ fn test_long_straddle_integration() -> Result<(), Box> { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal/strategy_long_strangle.rs b/tests/unit/strategies/optimal/strategy_long_strangle.rs index 1db1fb13..87b19ce4 100644 --- a/tests/unit/strategies/optimal/strategy_long_strangle.rs +++ b/tests/unit/strategies/optimal/strategy_long_strangle.rs @@ -31,7 +31,7 @@ fn test_long_strangle_integration() -> Result<(), Box> { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal/strategy_poor_mans_covered_call.rs b/tests/unit/strategies/optimal/strategy_poor_mans_covered_call.rs index 65f1ca92..48a7ee85 100644 --- a/tests/unit/strategies/optimal/strategy_poor_mans_covered_call.rs +++ b/tests/unit/strategies/optimal/strategy_poor_mans_covered_call.rs @@ -31,7 +31,7 @@ fn test_poor_mans_covered_call_integration() -> Result<(), Box> { pos_or_panic!(1.74), // close_fee_short_call pos_or_panic!(0.85), // open_fee_short_put pos_or_panic!(0.85), // close_fee_short_put - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal/strategy_short_butterfly_spread.rs b/tests/unit/strategies/optimal/strategy_short_butterfly_spread.rs index e7a9dbe7..693b5a94 100644 --- a/tests/unit/strategies/optimal/strategy_short_butterfly_spread.rs +++ b/tests/unit/strategies/optimal/strategy_short_butterfly_spread.rs @@ -35,7 +35,7 @@ fn test_short_butterfly_spread_integration() -> Result<(), Box> { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal/strategy_short_straddle.rs b/tests/unit/strategies/optimal/strategy_short_straddle.rs index 38cf5894..2bdc863a 100644 --- a/tests/unit/strategies/optimal/strategy_short_straddle.rs +++ b/tests/unit/strategies/optimal/strategy_short_straddle.rs @@ -30,7 +30,7 @@ fn test_short_straddle_integration() -> Result<(), Box> { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal/strategy_short_strangle.rs b/tests/unit/strategies/optimal/strategy_short_strangle.rs index eb85ce9a..e909add0 100644 --- a/tests/unit/strategies/optimal/strategy_short_strangle.rs +++ b/tests/unit/strategies/optimal/strategy_short_strangle.rs @@ -32,7 +32,7 @@ fn test_short_strangle_with_greeks_integration() -> Result<(), Box> { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal_center/strategy_bear_call_spread.rs b/tests/unit/strategies/optimal_center/strategy_bear_call_spread.rs index bb558144..5bfbf7ce 100644 --- a/tests/unit/strategies/optimal_center/strategy_bear_call_spread.rs +++ b/tests/unit/strategies/optimal_center/strategy_bear_call_spread.rs @@ -30,7 +30,7 @@ fn test_bear_call_spread_integration() -> Result<(), Box> { pos_or_panic!(0.78), // open_fee_long pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal_center/strategy_bear_put_spread.rs b/tests/unit/strategies/optimal_center/strategy_bear_put_spread.rs index e7455157..792c9f23 100644 --- a/tests/unit/strategies/optimal_center/strategy_bear_put_spread.rs +++ b/tests/unit/strategies/optimal_center/strategy_bear_put_spread.rs @@ -31,7 +31,7 @@ fn test_bear_put_spread_integration() -> Result<(), Box> { pos_or_panic!(0.78), // open_fee_long pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal_center/strategy_bull_call_spread.rs b/tests/unit/strategies/optimal_center/strategy_bull_call_spread.rs index 623ed73a..e3cf0ceb 100644 --- a/tests/unit/strategies/optimal_center/strategy_bull_call_spread.rs +++ b/tests/unit/strategies/optimal_center/strategy_bull_call_spread.rs @@ -31,7 +31,7 @@ fn test_bull_call_spread_integration() -> Result<(), Box> { pos_or_panic!(0.78), // open_fee_long pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal_center/strategy_bull_put_spread.rs b/tests/unit/strategies/optimal_center/strategy_bull_put_spread.rs index f0b3dbcf..934d453f 100644 --- a/tests/unit/strategies/optimal_center/strategy_bull_put_spread.rs +++ b/tests/unit/strategies/optimal_center/strategy_bull_put_spread.rs @@ -31,7 +31,7 @@ fn test_bull_put_spread_integration() -> Result<(), Box> { pos_or_panic!(0.78), // open_fee_long pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal_center/strategy_call_butterfly.rs b/tests/unit/strategies/optimal_center/strategy_call_butterfly.rs index a5606cb7..f7bd395b 100644 --- a/tests/unit/strategies/optimal_center/strategy_call_butterfly.rs +++ b/tests/unit/strategies/optimal_center/strategy_call_butterfly.rs @@ -36,7 +36,7 @@ fn test_call_butterfly_integration() -> Result<(), Box> { pos_or_panic!(0.73), // close_fee_short pos_or_panic!(0.73), // close_fee_short pos_or_panic!(0.73), // open_fee_short - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal_center/strategy_iron_butterfly.rs b/tests/unit/strategies/optimal_center/strategy_iron_butterfly.rs index 52765048..2212b5cf 100644 --- a/tests/unit/strategies/optimal_center/strategy_iron_butterfly.rs +++ b/tests/unit/strategies/optimal_center/strategy_iron_butterfly.rs @@ -32,7 +32,7 @@ fn test_iron_butterfly_integration() -> Result<(), Box> { pos_or_panic!(16.8), // premium_long_put pos_or_panic!(0.96), // open_fee pos_or_panic!(0.96), // close_fee - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal_center/strategy_iron_condor.rs b/tests/unit/strategies/optimal_center/strategy_iron_condor.rs index f610092d..3a89950e 100644 --- a/tests/unit/strategies/optimal_center/strategy_iron_condor.rs +++ b/tests/unit/strategies/optimal_center/strategy_iron_condor.rs @@ -33,7 +33,7 @@ fn test_iron_condor_integration() -> Result<(), Box> { pos_or_panic!(16.8), // premium_long_put pos_or_panic!(0.96), // open_fee pos_or_panic!(0.96), // close_fee - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal_center/strategy_long_butterfly_spread.rs b/tests/unit/strategies/optimal_center/strategy_long_butterfly_spread.rs index b1d98650..aeff11c4 100644 --- a/tests/unit/strategies/optimal_center/strategy_long_butterfly_spread.rs +++ b/tests/unit/strategies/optimal_center/strategy_long_butterfly_spread.rs @@ -35,7 +35,7 @@ fn test_long_butterfly_spread_integration() -> Result<(), Box> { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal_center/strategy_long_straddle.rs b/tests/unit/strategies/optimal_center/strategy_long_straddle.rs index 11fef4e1..8cc5fb10 100644 --- a/tests/unit/strategies/optimal_center/strategy_long_straddle.rs +++ b/tests/unit/strategies/optimal_center/strategy_long_straddle.rs @@ -30,7 +30,7 @@ fn test_long_straddle_integration() -> Result<(), Box> { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal_center/strategy_long_strangle.rs b/tests/unit/strategies/optimal_center/strategy_long_strangle.rs index 94895fd6..b91fd93e 100644 --- a/tests/unit/strategies/optimal_center/strategy_long_strangle.rs +++ b/tests/unit/strategies/optimal_center/strategy_long_strangle.rs @@ -31,7 +31,7 @@ fn test_long_strangle_integration() -> Result<(), Box> { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal_center/strategy_poor_mans_covered_call.rs b/tests/unit/strategies/optimal_center/strategy_poor_mans_covered_call.rs index f4b0741a..2c4e1273 100644 --- a/tests/unit/strategies/optimal_center/strategy_poor_mans_covered_call.rs +++ b/tests/unit/strategies/optimal_center/strategy_poor_mans_covered_call.rs @@ -31,7 +31,7 @@ fn test_poor_mans_covered_call_integration() -> Result<(), Box> { pos_or_panic!(1.74), // close_fee_short_call pos_or_panic!(0.85), // open_fee_short_put pos_or_panic!(0.85), // close_fee_short_put - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal_center/strategy_short_butterfly_spread.rs b/tests/unit/strategies/optimal_center/strategy_short_butterfly_spread.rs index 9a864b36..f04a4438 100644 --- a/tests/unit/strategies/optimal_center/strategy_short_butterfly_spread.rs +++ b/tests/unit/strategies/optimal_center/strategy_short_butterfly_spread.rs @@ -35,7 +35,7 @@ fn test_short_butterfly_spread_integration() -> Result<(), Box> { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal_center/strategy_short_straddle.rs b/tests/unit/strategies/optimal_center/strategy_short_straddle.rs index be0c194c..aa019742 100644 --- a/tests/unit/strategies/optimal_center/strategy_short_straddle.rs +++ b/tests/unit/strategies/optimal_center/strategy_short_straddle.rs @@ -30,7 +30,7 @@ fn test_short_straddle_integration() -> Result<(), Box> { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + )?; let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; diff --git a/tests/unit/strategies/optimal_center/strategy_short_strangle.rs b/tests/unit/strategies/optimal_center/strategy_short_strangle.rs index 8587b3c8..3a79d7a1 100644 --- a/tests/unit/strategies/optimal_center/strategy_short_strangle.rs +++ b/tests/unit/strategies/optimal_center/strategy_short_strangle.rs @@ -33,7 +33,7 @@ fn test_short_strangle_with_greeks_integration() -> Result<(), Box> { pos_or_panic!(0.1), // close_fee_short_call pos_or_panic!(0.1), // open_fee_short_put pos_or_panic!(0.1), // close_fee_short_put - ); + )?; let option_chain = OptionChain::load_from_json( "./examples/Chains/Germany-40-2025-05-27-15-29-00-UTC-24209.json", diff --git a/tests/unit/strategies/protective_put_test.rs b/tests/unit/strategies/protective_put_test.rs index fa0947c2..237b2041 100644 --- a/tests/unit/strategies/protective_put_test.rs +++ b/tests/unit/strategies/protective_put_test.rs @@ -34,6 +34,7 @@ fn create_test_protective_put() -> ProtectivePut { Positive::new(0.65).unwrap(), Positive::new(0.65).unwrap(), ) + .unwrap() } #[test] diff --git a/tests/unit/strategies/simple/strategy_bear_call_spread.rs b/tests/unit/strategies/simple/strategy_bear_call_spread.rs index 4c62a969..566af3d5 100644 --- a/tests/unit/strategies/simple/strategy_bear_call_spread.rs +++ b/tests/unit/strategies/simple/strategy_bear_call_spread.rs @@ -28,7 +28,7 @@ fn test_bear_call_spread_integration() -> Result<(), Box> { pos_or_panic!(0.78), // open_fee_long pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short - ); + )?; // Assertions to validate strategy properties and computations assert_eq!( diff --git a/tests/unit/strategies/simple/strategy_bear_put_spread.rs b/tests/unit/strategies/simple/strategy_bear_put_spread.rs index e3cfe676..53cff7da 100644 --- a/tests/unit/strategies/simple/strategy_bear_put_spread.rs +++ b/tests/unit/strategies/simple/strategy_bear_put_spread.rs @@ -30,7 +30,7 @@ fn test_bear_put_spread_integration() -> Result<(), Box> { pos_or_panic!(0.78), // open_fee_long pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short - ); + )?; // Assertions to validate strategy properties and computations assert_eq!( diff --git a/tests/unit/strategies/simple/strategy_bull_call_spread.rs b/tests/unit/strategies/simple/strategy_bull_call_spread.rs index 1f35b4a6..c0ec898f 100644 --- a/tests/unit/strategies/simple/strategy_bull_call_spread.rs +++ b/tests/unit/strategies/simple/strategy_bull_call_spread.rs @@ -30,7 +30,7 @@ fn test_bull_call_spread_integration() -> Result<(), Box> { pos_or_panic!(0.78), // open_fee_long pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short - ); + )?; // Assertions to validate strategy properties and computations assert_eq!( diff --git a/tests/unit/strategies/simple/strategy_bull_put_spread.rs b/tests/unit/strategies/simple/strategy_bull_put_spread.rs index ffae8eb3..5edaff1e 100644 --- a/tests/unit/strategies/simple/strategy_bull_put_spread.rs +++ b/tests/unit/strategies/simple/strategy_bull_put_spread.rs @@ -30,7 +30,7 @@ fn test_bull_put_spread_integration() -> Result<(), Box> { pos_or_panic!(0.78), // open_fee_long pos_or_panic!(0.73), // close_fee_long pos_or_panic!(0.73), // close_fee_short - ); + )?; // Assertions to validate strategy properties and computations assert_eq!( diff --git a/tests/unit/strategies/simple/strategy_call_butterfly.rs b/tests/unit/strategies/simple/strategy_call_butterfly.rs index 06acabd3..92cd9d32 100644 --- a/tests/unit/strategies/simple/strategy_call_butterfly.rs +++ b/tests/unit/strategies/simple/strategy_call_butterfly.rs @@ -34,7 +34,7 @@ fn test_call_butterfly_integration() -> Result<(), Box> { pos_or_panic!(0.73), // close_fee_short pos_or_panic!(0.73), // close_fee_short pos_or_panic!(0.73), // open_fee_short - ); + )?; // Assertions to validate strategy properties and computations assert_eq!( diff --git a/tests/unit/strategies/simple/strategy_graph.rs b/tests/unit/strategies/simple/strategy_graph.rs index 9c3ad5a8..4e11b674 100644 --- a/tests/unit/strategies/simple/strategy_graph.rs +++ b/tests/unit/strategies/simple/strategy_graph.rs @@ -27,7 +27,7 @@ fn test_bull_call_spread_basic_integration() -> Result<(), Box> { pos_or_panic!(0.58), // close_fee_long pos_or_panic!(0.55), // close_fee_short pos_or_panic!(0.54), // open_fee_short - ); + )?; // Validate strategy properties assert_eq!( diff --git a/tests/unit/strategies/simple/strategy_iron_butterfly.rs b/tests/unit/strategies/simple/strategy_iron_butterfly.rs index 8ee168ed..8bd7362d 100644 --- a/tests/unit/strategies/simple/strategy_iron_butterfly.rs +++ b/tests/unit/strategies/simple/strategy_iron_butterfly.rs @@ -31,7 +31,7 @@ fn test_iron_butterfly_integration() -> Result<(), Box> { pos_or_panic!(16.8), // premium_long_put pos_or_panic!(0.96), // open_fee pos_or_panic!(0.96), // close_fee - ); + )?; // Validate strategy assert!(strategy.validate(), "Strategy should be valid"); diff --git a/tests/unit/strategies/simple/strategy_iron_condor.rs b/tests/unit/strategies/simple/strategy_iron_condor.rs index 8e883ede..27770d2f 100644 --- a/tests/unit/strategies/simple/strategy_iron_condor.rs +++ b/tests/unit/strategies/simple/strategy_iron_condor.rs @@ -32,7 +32,7 @@ fn test_iron_condor_integration() -> Result<(), Box> { pos_or_panic!(16.8), // premium_long_put pos_or_panic!(0.96), // open_fee pos_or_panic!(0.96), // close_fee - ); + )?; // Validate strategy assert!(strategy.validate(), "Strategy should be valid"); diff --git a/tests/unit/strategies/simple/strategy_long_butterfly_spread.rs b/tests/unit/strategies/simple/strategy_long_butterfly_spread.rs index c7576e33..c49a4600 100644 --- a/tests/unit/strategies/simple/strategy_long_butterfly_spread.rs +++ b/tests/unit/strategies/simple/strategy_long_butterfly_spread.rs @@ -35,7 +35,7 @@ fn test_long_butterfly_spread_integration() -> Result<(), Box> { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + )?; // Assertions to validate strategy properties and computations assert_eq!(strategy.get_break_even_points().unwrap().len(), 2); diff --git a/tests/unit/strategies/simple/strategy_long_straddle.rs b/tests/unit/strategies/simple/strategy_long_straddle.rs index fc19fec6..af1a0c26 100644 --- a/tests/unit/strategies/simple/strategy_long_straddle.rs +++ b/tests/unit/strategies/simple/strategy_long_straddle.rs @@ -31,7 +31,7 @@ fn test_long_straddle_integration() -> Result<(), Box> { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + )?; // Assertions to validate strategy properties and computations assert_eq!( diff --git a/tests/unit/strategies/simple/strategy_long_strangle.rs b/tests/unit/strategies/simple/strategy_long_strangle.rs index 5c492993..26de7c0d 100644 --- a/tests/unit/strategies/simple/strategy_long_strangle.rs +++ b/tests/unit/strategies/simple/strategy_long_strangle.rs @@ -32,7 +32,7 @@ fn test_long_strangle_integration() -> Result<(), Box> { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + )?; // Assertions to validate strategy properties and computations assert_eq!( diff --git a/tests/unit/strategies/simple/strategy_poor_mans_covered_call.rs b/tests/unit/strategies/simple/strategy_poor_mans_covered_call.rs index 292b0101..5ba62a3c 100644 --- a/tests/unit/strategies/simple/strategy_poor_mans_covered_call.rs +++ b/tests/unit/strategies/simple/strategy_poor_mans_covered_call.rs @@ -30,7 +30,7 @@ fn test_poor_mans_covered_call_integration() -> Result<(), Box> { pos_or_panic!(1.74), // close_fee_short_call pos_or_panic!(0.85), // open_fee_short_put pos_or_panic!(0.85), // close_fee_short_put - ); + )?; // Assertions to validate strategy properties and computations assert_eq!(strategy.get_break_even_points().unwrap().len(), 1); diff --git a/tests/unit/strategies/simple/strategy_short_butterfly_spread.rs b/tests/unit/strategies/simple/strategy_short_butterfly_spread.rs index de945c42..9ae5c7b3 100644 --- a/tests/unit/strategies/simple/strategy_short_butterfly_spread.rs +++ b/tests/unit/strategies/simple/strategy_short_butterfly_spread.rs @@ -34,7 +34,7 @@ fn test_short_butterfly_spread_integration() -> Result<(), Box> { Positive::ZERO, Positive::ZERO, Positive::ZERO, - ); + )?; // Assertions to validate strategy properties and computations assert_eq!(strategy.get_break_even_points().unwrap().len(), 1); diff --git a/tests/unit/strategies/simple/strategy_short_straddle.rs b/tests/unit/strategies/simple/strategy_short_straddle.rs index 52d38085..d6bcc302 100644 --- a/tests/unit/strategies/simple/strategy_short_straddle.rs +++ b/tests/unit/strategies/simple/strategy_short_straddle.rs @@ -29,7 +29,7 @@ fn test_short_straddle_integration() -> Result<(), Box> { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + )?; // Assertions to validate strategy properties and computations assert_eq!(strategy.get_break_even_points().unwrap().len(), 2); diff --git a/tests/unit/strategies/simple/strategy_short_strangle.rs b/tests/unit/strategies/simple/strategy_short_strangle.rs index 7057d0d3..7ca8238b 100644 --- a/tests/unit/strategies/simple/strategy_short_strangle.rs +++ b/tests/unit/strategies/simple/strategy_short_strangle.rs @@ -32,7 +32,7 @@ fn test_short_strangle_with_greeks_integration() -> Result<(), Box> { pos_or_panic!(7.01), // close_fee_short_call pos_or_panic!(7.01), // open_fee_short_put pos_or_panic!(7.01), // close_fee_short_put - ); + )?; // Assertions to validate strategy properties and computations assert_eq!(strategy.get_break_even_points().unwrap().len(), 2);