diff --git a/Cargo.toml b/Cargo.toml index 7bad3cb7..6ece704a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "optionstratlib" -version = "0.2.4" +version = "0.2.5" edition = "2021" authors = ["Joaquin Bejar "] description = "OptionStratLib is a comprehensive Rust library for options trading and strategy development across multiple asset classes." diff --git a/Draws/Options/intrinsic_value_chart.png b/Draws/Options/intrinsic_value_chart.png index 9d9b8823..55721c89 100644 Binary files a/Draws/Options/intrinsic_value_chart.png and b/Draws/Options/intrinsic_value_chart.png differ diff --git a/Draws/Strategy/bear_call_spread_profit_loss_chart.png b/Draws/Strategy/bear_call_spread_profit_loss_chart.png index cb0c7fc4..63e1c959 100644 Binary files a/Draws/Strategy/bear_call_spread_profit_loss_chart.png and b/Draws/Strategy/bear_call_spread_profit_loss_chart.png differ diff --git a/Draws/Strategy/bear_call_spread_profit_loss_chart_best_area.png b/Draws/Strategy/bear_call_spread_profit_loss_chart_best_area.png index 98fe3599..4beda17b 100644 Binary files a/Draws/Strategy/bear_call_spread_profit_loss_chart_best_area.png and b/Draws/Strategy/bear_call_spread_profit_loss_chart_best_area.png differ diff --git a/Draws/Strategy/bear_call_spread_profit_loss_chart_best_ratio.png b/Draws/Strategy/bear_call_spread_profit_loss_chart_best_ratio.png index c11c2f2b..ba94c639 100644 Binary files a/Draws/Strategy/bear_call_spread_profit_loss_chart_best_ratio.png and b/Draws/Strategy/bear_call_spread_profit_loss_chart_best_ratio.png differ diff --git a/Draws/Strategy/bull_call_spread_profit_loss_chart.png b/Draws/Strategy/bull_call_spread_profit_loss_chart.png index cb2cf678..bab9e2b0 100644 Binary files a/Draws/Strategy/bull_call_spread_profit_loss_chart.png and b/Draws/Strategy/bull_call_spread_profit_loss_chart.png differ diff --git a/Draws/Strategy/bull_put_spread_profit_loss_chart_best_ratio.png b/Draws/Strategy/bull_put_spread_profit_loss_chart_best_ratio.png index ef2c3fe2..5e14fcd8 100644 Binary files a/Draws/Strategy/bull_put_spread_profit_loss_chart_best_ratio.png and b/Draws/Strategy/bull_put_spread_profit_loss_chart_best_ratio.png differ diff --git a/Draws/Strategy/call_butterfly_profit_loss_chart.png b/Draws/Strategy/call_butterfly_profit_loss_chart.png index d20c1326..89d549e2 100644 Binary files a/Draws/Strategy/call_butterfly_profit_loss_chart.png and b/Draws/Strategy/call_butterfly_profit_loss_chart.png differ diff --git a/Draws/Strategy/call_butterfly_profit_loss_chart_best_area.png b/Draws/Strategy/call_butterfly_profit_loss_chart_best_area.png index 5d9157e4..77742b2b 100644 Binary files a/Draws/Strategy/call_butterfly_profit_loss_chart_best_area.png and b/Draws/Strategy/call_butterfly_profit_loss_chart_best_area.png differ diff --git a/Draws/Strategy/call_butterfly_profit_loss_chart_best_ratio.png b/Draws/Strategy/call_butterfly_profit_loss_chart_best_ratio.png index db2709fc..77742b2b 100644 Binary files a/Draws/Strategy/call_butterfly_profit_loss_chart_best_ratio.png and b/Draws/Strategy/call_butterfly_profit_loss_chart_best_ratio.png differ diff --git a/Draws/Strategy/iron_butterfly_profit_loss_chart_best_area.png b/Draws/Strategy/iron_butterfly_profit_loss_chart_best_area.png index ea4da2f0..6d69384c 100644 Binary files a/Draws/Strategy/iron_butterfly_profit_loss_chart_best_area.png and b/Draws/Strategy/iron_butterfly_profit_loss_chart_best_area.png differ diff --git a/Draws/Strategy/iron_butterfly_profit_loss_chart_best_ratio.png b/Draws/Strategy/iron_butterfly_profit_loss_chart_best_ratio.png index d67c0429..adad368d 100644 Binary files a/Draws/Strategy/iron_butterfly_profit_loss_chart_best_ratio.png and b/Draws/Strategy/iron_butterfly_profit_loss_chart_best_ratio.png differ diff --git a/Draws/Strategy/iron_condor_profit_loss_chart_best_area.png b/Draws/Strategy/iron_condor_profit_loss_chart_best_area.png index 1f6b8815..9fb54349 100644 Binary files a/Draws/Strategy/iron_condor_profit_loss_chart_best_area.png and b/Draws/Strategy/iron_condor_profit_loss_chart_best_area.png differ diff --git a/Draws/Strategy/iron_condor_profit_loss_chart_best_ratio.png b/Draws/Strategy/iron_condor_profit_loss_chart_best_ratio.png index 60ef8ced..7f696500 100644 Binary files a/Draws/Strategy/iron_condor_profit_loss_chart_best_ratio.png and b/Draws/Strategy/iron_condor_profit_loss_chart_best_ratio.png differ diff --git a/Draws/Strategy/long_butterfly_spread_profit_loss_chart.png b/Draws/Strategy/long_butterfly_spread_profit_loss_chart.png index a6c9b307..5a238b30 100644 Binary files a/Draws/Strategy/long_butterfly_spread_profit_loss_chart.png and b/Draws/Strategy/long_butterfly_spread_profit_loss_chart.png differ diff --git a/Draws/Strategy/long_butterfly_spread_profit_loss_chart_best_area.png b/Draws/Strategy/long_butterfly_spread_profit_loss_chart_best_area.png index 10b4273e..5c50a631 100644 Binary files a/Draws/Strategy/long_butterfly_spread_profit_loss_chart_best_area.png and b/Draws/Strategy/long_butterfly_spread_profit_loss_chart_best_area.png differ diff --git a/Draws/Strategy/long_straddle_profit_loss_chart_best_area.png b/Draws/Strategy/long_straddle_profit_loss_chart_best_area.png index c2269fa7..05a5bc00 100644 Binary files a/Draws/Strategy/long_straddle_profit_loss_chart_best_area.png and b/Draws/Strategy/long_straddle_profit_loss_chart_best_area.png differ diff --git a/Draws/Strategy/long_straddle_profit_loss_chart_best_ratio.png b/Draws/Strategy/long_straddle_profit_loss_chart_best_ratio.png index 3951e82f..9c7b9e63 100644 Binary files a/Draws/Strategy/long_straddle_profit_loss_chart_best_ratio.png and b/Draws/Strategy/long_straddle_profit_loss_chart_best_ratio.png differ diff --git a/Draws/Strategy/long_strangle_profit_loss_chart_best_area.png b/Draws/Strategy/long_strangle_profit_loss_chart_best_area.png index 235bd5bb..cb47db86 100644 Binary files a/Draws/Strategy/long_strangle_profit_loss_chart_best_area.png and b/Draws/Strategy/long_strangle_profit_loss_chart_best_area.png differ diff --git a/Draws/Strategy/poor_mans_covered_call_profit_loss_chart_best_ratio.png b/Draws/Strategy/poor_mans_covered_call_profit_loss_chart_best_ratio.png index f817d616..e1f27501 100644 Binary files a/Draws/Strategy/poor_mans_covered_call_profit_loss_chart_best_ratio.png and b/Draws/Strategy/poor_mans_covered_call_profit_loss_chart_best_ratio.png differ diff --git a/Draws/Strategy/short_butterfly_spread_profit_loss_chart_best_area.png b/Draws/Strategy/short_butterfly_spread_profit_loss_chart_best_area.png index 164d1b5e..49f9e077 100644 Binary files a/Draws/Strategy/short_butterfly_spread_profit_loss_chart_best_area.png and b/Draws/Strategy/short_butterfly_spread_profit_loss_chart_best_area.png differ diff --git a/Draws/Strategy/short_butterfly_spread_profit_loss_chart_best_ratio.png b/Draws/Strategy/short_butterfly_spread_profit_loss_chart_best_ratio.png index e3479d8e..c285f6b6 100644 Binary files a/Draws/Strategy/short_butterfly_spread_profit_loss_chart_best_ratio.png and b/Draws/Strategy/short_butterfly_spread_profit_loss_chart_best_ratio.png differ diff --git a/Draws/Strategy/short_straddle_profit_loss_chart_best_area.png b/Draws/Strategy/short_straddle_profit_loss_chart_best_area.png index 145b7ac3..b6edfa22 100644 Binary files a/Draws/Strategy/short_straddle_profit_loss_chart_best_area.png and b/Draws/Strategy/short_straddle_profit_loss_chart_best_area.png differ diff --git a/Draws/Strategy/short_straddle_profit_loss_chart_best_ratio.png b/Draws/Strategy/short_straddle_profit_loss_chart_best_ratio.png index aff84078..4f43225b 100644 Binary files a/Draws/Strategy/short_straddle_profit_loss_chart_best_ratio.png and b/Draws/Strategy/short_straddle_profit_loss_chart_best_ratio.png differ diff --git a/Draws/Strategy/short_strangle_delta_profit_loss_chart.png b/Draws/Strategy/short_strangle_delta_profit_loss_chart.png index 08a8b313..4b32022a 100644 Binary files a/Draws/Strategy/short_strangle_delta_profit_loss_chart.png and b/Draws/Strategy/short_strangle_delta_profit_loss_chart.png differ diff --git a/Draws/Strategy/short_strangle_profit_loss_chart.png b/Draws/Strategy/short_strangle_profit_loss_chart.png index 97ba6c27..45b935d5 100644 Binary files a/Draws/Strategy/short_strangle_profit_loss_chart.png and b/Draws/Strategy/short_strangle_profit_loss_chart.png differ diff --git a/Draws/Strategy/short_strangle_profit_loss_chart_best_area.png b/Draws/Strategy/short_strangle_profit_loss_chart_best_area.png index ef166f04..4687481d 100644 Binary files a/Draws/Strategy/short_strangle_profit_loss_chart_best_area.png and b/Draws/Strategy/short_strangle_profit_loss_chart_best_area.png differ diff --git a/Draws/Strategy/short_strangle_profit_loss_chart_best_ratio.png b/Draws/Strategy/short_strangle_profit_loss_chart_best_ratio.png index f6116612..154e1b22 100644 Binary files a/Draws/Strategy/short_strangle_profit_loss_chart_best_ratio.png and b/Draws/Strategy/short_strangle_profit_loss_chart_best_ratio.png differ diff --git a/Makefile b/Makefile index 20cf6f52..087ba6ad 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,8 @@ # Makefile for common tasks in a Rust project # Detect current branch CURRENT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD) +ZIP_NAME = OptionStratLib.zip + # Default target .PHONY: all @@ -18,7 +20,7 @@ release: # Run tests .PHONY: test test: - cargo test + LOGLEVEL=WARN cargo test # Format the code .PHONY: fmt @@ -102,4 +104,15 @@ readme: create-doc .PHONY: check-spanish check-spanish: - cd scripts && python3 spanish.py ../src && cd .. \ No newline at end of file + cd scripts && python3 spanish.py ../src && cd .. + +.PHONY: zip +zip: + @echo "Creating $(ZIP_NAME) without any 'target' directories, 'Cargo.lock', and hidden files..." + @find . -type f \ + ! -path "*/target/*" \ + ! -path "./.*" \ + ! -name "Cargo.lock" \ + ! -name ".*" \ + | zip -@ $(ZIP_NAME) + @echo "$(ZIP_NAME) created successfully." \ No newline at end of file diff --git a/README.md b/README.md index 80c5ec4a..0fb22603 100644 --- a/README.md +++ b/README.md @@ -2,19 +2,23 @@
- OptionStratLib + OptionStratLib
[![Dual License](https://img.shields.io/badge/license-MIT%20and%20Apache%202.0-blue)](./LICENSE) [![Crates.io](https://img.shields.io/crates/v/optionstratlib.svg)](https://crates.io/crates/optionstratlib) [![Downloads](https://img.shields.io/crates/d/optionstratlib.svg)](https://crates.io/crates/optionstratlib) [![Stars](https://img.shields.io/github/stars/joaquinbejar/OptionStratLib.svg)](https://github.com/joaquinbejar/OptionStratLib/stargazers) + [![Issues](https://img.shields.io/github/issues/joaquinbejar/OptionStratLib.svg)](https://github.com/joaquinbejar/OptionStratLib/issues) + [![PRs](https://img.shields.io/github/issues-pr/joaquinbejar/OptionStratLib.svg)](https://github.com/joaquinbejar/OptionStratLib/pulls) + [![Build Status](https://img.shields.io/github/workflow/status/joaquinbejar/OptionStratLib/CI)](https://github.com/joaquinbejar/OptionStratLib/actions) [![Coverage](https://img.shields.io/codecov/c/github/joaquinbejar/OptionStratLib)](https://codecov.io/gh/joaquinbejar/OptionStratLib) [![Dependencies](https://img.shields.io/librariesio/github/joaquinbejar/OptionStratLib)](https://libraries.io/github/joaquinbejar/OptionStratLib) + [![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://docs.rs/optionstratlib) - # OptionStratLib v0.2.4: Financial Options Library + # OptionStratLib v0.2.5: Financial Options Library ## Table of Contents 1. [Introduction](#introduction) diff --git a/examples/examples_chain/src/bin/option_chain.rs b/examples/examples_chain/src/bin/option_chain.rs index 80becd17..42b933ec 100644 --- a/examples/examples_chain/src/bin/option_chain.rs +++ b/examples/examples_chain/src/bin/option_chain.rs @@ -11,7 +11,13 @@ use tracing::info; fn main() { setup_logger(); - let mut chain = OptionChain::new("SP500", pos!(5781.88), "18 oct 2024".to_string()); + let mut chain = OptionChain::new( + "SP500", + pos!(5781.88), + "18 oct 2024".to_string(), + None, + None, + ); chain.add_option( pos!(5520.0), diff --git a/examples/examples_strategies/src/bin/strategy_call_butterfly.rs b/examples/examples_strategies/src/bin/strategy_call_butterfly.rs index 3c12378b..be9df38f 100644 --- a/examples/examples_strategies/src/bin/strategy_call_butterfly.rs +++ b/examples/examples_strategies/src/bin/strategy_call_butterfly.rs @@ -21,26 +21,26 @@ fn main() -> Result<(), Box> { let strategy = CallButterfly::new( "SP500".to_string(), underlying_price, // underlying_price - pos!(5750.0), // long_strike_itm - pos!(5850.0), // long_strike_otm - pos!(5800.0), // short_strike + pos!(5750.0), // long_call_strike + pos!(5800.0), // short_call_low_strike + pos!(5850.0), // short_call_high_strike ExpirationDate::Days(2.0), 0.18, // implied_volatility 0.05, // risk_free_rate 0.0, // dividend_yield pos!(1.0), // long quantity - pos!(2.0), // short_quantity 85.04, // premium_long_itm - 31.65, // premium_long_otm - 53.04, // premium_short + 53.04, // premium_long_otm + 28.85, // premium_short + 0.78, // premium_short 0.78, // open_fee_long 0.78, // close_fee_long 0.73, // close_fee_short 0.73, // close_fee_short + 0.72, // open_fee_short ); - let price_range = strategy.best_range_to_show(pos!(1.0)).unwrap(); - let range = strategy.break_even_points[1] - strategy.break_even_points[0]; + let range = strategy.range_of_profit().unwrap_or(PZERO); info!("Title: {}", strategy.title()); info!("Break Even Points: {:?}", strategy.break_even_points); 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 518f883c..69ba401c 100644 --- a/examples/examples_strategies/src/bin/strategy_long_butterfly_spread.rs +++ b/examples/examples_strategies/src/bin/strategy_long_butterfly_spread.rs @@ -4,6 +4,7 @@ Date: 25/9/24 ******************************************************************************/ +use optionstratlib::greeks::equations::Greeks; use optionstratlib::model::types::PositiveF64; use optionstratlib::model::types::{ExpirationDate, PZERO}; use optionstratlib::pos; @@ -17,40 +18,24 @@ use tracing::info; fn main() -> Result<(), Box> { setup_logger(); - let underlying_price = pos!(5781.88); + let underlying_price = pos!(5795.88); let strategy = LongButterflySpread::new( "SP500".to_string(), underlying_price, // underlying_price - pos!(5810.0), // long_strike_itm - pos!(5820.0), // short_strike - pos!(6200.0), // long_strike_otm + pos!(5710.0), // long_strike_itm + pos!(5780.0), // short_strike + pos!(5850.0), // long_strike_otm ExpirationDate::Days(2.0), 0.18, // implied_volatility 0.05, // risk_free_rate 0.0, // dividend_yield pos!(1.0), // long quantity - 49.65, // premium_long - 42.93, // premium_short - 1.0, // open_fee_long - 4.0, // open_fee_long + 113.30, // premium_long + 64.20, // premium_short + 31.65, // open_fee_long + 0.07, // open_fee_long ); - // let strategy = LongButterfly::new( - // "SP500".to_string(), - // underlying_price, // underlying_price - // pos!(5730.0), // long_strike_itm - // pos!(5740.0), // short_strike - // pos!(5850.0), // long_strike_otm - // ExpirationDate::Days(2.0), - // 0.18, // implied_volatility - // 0.05, // risk_free_rate - // 0.0, // dividend_yield - // pos!(1.0), // long quantity - // 98.79, // premium_long - // 90.02, // premium_short - // 31.65, // open_fee_long - // 4.0, // open_fee_long - // ); let price_range = strategy.best_range_to_show(pos!(1.0)).unwrap(); @@ -74,5 +59,7 @@ fn main() -> Result<(), Box> { (1400, 933), )?; + info!("Greeks: {:#?}", strategy.greeks()); + Ok(()) } diff --git a/examples/examples_strategies/src/bin/strategy_short_strangle.rs b/examples/examples_strategies/src/bin/strategy_short_strangle.rs index 8257d6c9..8ae4d3cd 100644 --- a/examples/examples_strategies/src/bin/strategy_short_strangle.rs +++ b/examples/examples_strategies/src/bin/strategy_short_strangle.rs @@ -1,3 +1,4 @@ +use optionstratlib::greeks::equations::Greeks; use optionstratlib::model::types::PositiveF64; use optionstratlib::model::types::{ExpirationDate, PZERO}; use optionstratlib::pos; @@ -58,5 +59,7 @@ fn main() -> Result<(), Box> { (1400, 933), )?; + info!("Greeks: {:#?}", strategy.greeks()); + Ok(()) } diff --git a/examples/examples_strategies/src/bin/strategy_short_strangle_delta.rs b/examples/examples_strategies/src/bin/strategy_short_strangle_delta.rs index ec1f1514..39180437 100644 --- a/examples/examples_strategies/src/bin/strategy_short_strangle_delta.rs +++ b/examples/examples_strategies/src/bin/strategy_short_strangle_delta.rs @@ -11,24 +11,24 @@ use tracing::info; fn main() -> Result<(), Box> { setup_logger(); - let underlying_price = pos!(2655.6); + let underlying_price = pos!(7250.6); let strategy = ShortStrangle::new( - "GOLD".to_string(), + "CL".to_string(), underlying_price, // underlying_price - pos!(2480.0), // call_strike - pos!(2650.0), // put_strike - ExpirationDate::Days(3.0), - 0.1548, // implied_volatility + pos!(7450.0), // call_strike + pos!(7050.0), // put_strike + ExpirationDate::Days(45.0), + 0.3745, // implied_volatility 0.05, // risk_free_rate 0.0, // dividend_yield pos!(2.0), // quantity - 46.3, // premium_short_call - 4.6, // premium_short_put - 0.96, // open_fee_short_call - 0.96, // close_fee_short_call - 0.96, // open_fee_short_put - 0.96, // close_fee_short_put + 84.2, // premium_short_call + 353.2, // premium_short_put + 7.01, // open_fee_short_call + 7.01, // close_fee_short_call + 7.01, // open_fee_short_put + 7.01, // close_fee_short_put ); let price_range = strategy.best_range_to_show(pos!(1.0)).unwrap(); 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 0c8c5a5e..4fdd0304 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 @@ -54,7 +54,7 @@ fn main() -> Result<(), Box> { range, (range / 2.0) / underlying_price * 100.0 ); - info!("Profit Ratio: {:.2}%", strategy.profit_ratio()); + info!("Profit Area: {:.2}%", strategy.profit_area()); if strategy.profit_ratio() > ZERO { 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 e3625475..520a94f7 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 @@ -16,7 +16,6 @@ fn main() -> Result<(), Box> { let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; - // println!("{}", option_chain); let underlying_price = option_chain.underlying_price; let mut strategy = BullCallSpread::new( "SP500".to_string(), 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 1ece420b..809ef4d1 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 @@ -16,7 +16,6 @@ fn main() -> Result<(), Box> { let option_chain = OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; - // println!("{}", option_chain); let underlying_price = option_chain.underlying_price; let mut strategy = BullCallSpread::new( "SP500".to_string(), 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 18e1ed23..253678d4 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 @@ -13,7 +13,6 @@ use optionstratlib::strategies::call_butterfly::CallButterfly; use optionstratlib::strategies::utils::FindOptimalSide; use optionstratlib::utils::logger::setup_logger; use optionstratlib::visualization::utils::Graph; -use std::env; use std::error::Error; use tracing::{debug, info}; @@ -33,7 +32,7 @@ fn main() -> Result<(), Box> { 0.05, // risk_free_rate ZERO, // dividend_yield pos!(2.0), // long quantity - pos!(4.0), // short_quantity + ZERO, // short_quantity ZERO, // premium_long_itm ZERO, // premium_long_otm ZERO, // premium_short @@ -41,6 +40,7 @@ fn main() -> Result<(), Box> { 0.78, // close_fee_long 0.73, // close_fee_short 0.73, // close_fee_short + 0.73, // close_fee_short ); strategy.best_area( 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 c8b2c04b..067d6402 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 @@ -32,7 +32,7 @@ fn main() -> Result<(), Box> { 0.05, // risk_free_rate ZERO, // dividend_yield pos!(2.0), // long quantity - pos!(4.0), // short_quantity + ZERO, // short_quantity ZERO, // premium_long_itm ZERO, // premium_long_otm ZERO, // premium_short @@ -40,6 +40,7 @@ fn main() -> Result<(), Box> { 0.78, // close_fee_long 0.73, // close_fee_short 0.73, // close_fee_short + 0.73, ); strategy.best_ratio( 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 159d2eb9..9d0f3365 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 @@ -1,5 +1,6 @@ use optionstratlib::chains::chain::OptionChain; use optionstratlib::constants::ZERO; +use optionstratlib::greeks::equations::Greeks; use optionstratlib::model::types::PositiveF64; use optionstratlib::model::types::{ExpirationDate, PZERO}; use optionstratlib::pos; @@ -69,5 +70,7 @@ fn main() -> Result<(), Box> { )?; } + info!("Greeks: {:#?}", strategy.greeks()); + Ok(()) } 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 93b44b41..91444eff 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 @@ -1,5 +1,6 @@ use optionstratlib::chains::chain::OptionChain; use optionstratlib::constants::ZERO; +use optionstratlib::greeks::equations::Greeks; use optionstratlib::model::types::PositiveF64; use optionstratlib::model::types::{ExpirationDate, PZERO}; use optionstratlib::pos; @@ -68,6 +69,7 @@ fn main() -> Result<(), Box> { (1400, 933), )?; } + info!("Greeks: {:#?}", strategy.greeks()); Ok(()) } 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 27c389dd..487b4204 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 @@ -1,5 +1,6 @@ use optionstratlib::chains::chain::OptionChain; use optionstratlib::constants::ZERO; +use optionstratlib::greeks::equations::Greeks; use optionstratlib::model::types::PositiveF64; use optionstratlib::model::types::{ExpirationDate, PZERO}; use optionstratlib::pos; @@ -69,6 +70,6 @@ fn main() -> Result<(), Box> { (1400, 933), )?; } - + info!("Greeks: {:#?}", strategy.greeks()); Ok(()) } 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 cef84461..509c6849 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 @@ -1,5 +1,6 @@ use optionstratlib::chains::chain::OptionChain; use optionstratlib::constants::ZERO; +use optionstratlib::greeks::equations::Greeks; use optionstratlib::model::types::PositiveF64; use optionstratlib::model::types::{ExpirationDate, PZERO}; use optionstratlib::pos; @@ -69,6 +70,6 @@ fn main() -> Result<(), Box> { (1400, 933), )?; } - + info!("Greeks: {:#?}", strategy.greeks()); Ok(()) } 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 865ea254..fdedee41 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 @@ -1,5 +1,6 @@ use optionstratlib::chains::chain::OptionChain; use optionstratlib::constants::ZERO; +use optionstratlib::greeks::equations::Greeks; use optionstratlib::model::types::PositiveF64; use optionstratlib::model::types::{ExpirationDate, PZERO}; use optionstratlib::pos; @@ -34,7 +35,6 @@ fn main() -> Result<(), Box> { 0.82, // close_fee_short_put ); strategy.best_area(&option_chain, FindOptimalSide::All); - // info!("Option Chain: {}", option_chain); debug!("Strategy: {:#?}", strategy); let price_range = strategy.best_range_to_show(pos!(1.0)).unwrap(); let range = strategy.range_of_profit().unwrap_or(PZERO); @@ -63,6 +63,6 @@ fn main() -> Result<(), Box> { (1400, 933), )?; } - + info!("Greeks: {:#?}", strategy.greeks()); Ok(()) } 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 8b4649e5..d7c7ee82 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 @@ -1,5 +1,6 @@ use optionstratlib::chains::chain::OptionChain; use optionstratlib::constants::ZERO; +use optionstratlib::greeks::equations::Greeks; use optionstratlib::model::types::PositiveF64; use optionstratlib::model::types::{ExpirationDate, PZERO}; use optionstratlib::pos; @@ -62,6 +63,6 @@ fn main() -> Result<(), Box> { (1400, 933), )?; } - + info!("Greeks: {:#?}", strategy.greeks()); Ok(()) } 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 a3a600f7..1fbd323c 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 @@ -1,5 +1,6 @@ use optionstratlib::chains::chain::OptionChain; use optionstratlib::constants::ZERO; +use optionstratlib::greeks::equations::Greeks; use optionstratlib::model::types::PositiveF64; use optionstratlib::model::types::{ExpirationDate, PZERO}; use optionstratlib::pos; @@ -65,5 +66,7 @@ fn main() -> Result<(), Box> { )?; } + info!("Greeks: {:#?}", strategy.greeks()); + Ok(()) } 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 e4bfd12f..3760085c 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 @@ -1,5 +1,6 @@ use optionstratlib::chains::chain::OptionChain; use optionstratlib::constants::ZERO; +use optionstratlib::greeks::equations::Greeks; use optionstratlib::model::types::PositiveF64; use optionstratlib::model::types::{ExpirationDate, PZERO}; use optionstratlib::pos; @@ -34,7 +35,7 @@ fn main() -> Result<(), Box> { 0.82, // close_fee_short_put ); // strategy.best_area(&option_chain, FindOptimalSide::Range(pos!(5700.0), pos!(6100.0))); - strategy.best_area(&option_chain, FindOptimalSide::All); + strategy.best_area(&option_chain, FindOptimalSide::Upper); debug!("Strategy: {:#?}", strategy); let price_range = strategy.best_range_to_show(pos!(1.0)).unwrap(); let range = strategy.range_of_profit().unwrap_or(PZERO); @@ -64,5 +65,7 @@ fn main() -> Result<(), Box> { )?; } + info!("Greeks: {:#?}", strategy.greeks()); + Ok(()) } 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 5b14d27e..76b48db9 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 @@ -1,5 +1,6 @@ use optionstratlib::chains::chain::OptionChain; use optionstratlib::constants::ZERO; +use optionstratlib::greeks::equations::Greeks; use optionstratlib::model::types::PositiveF64; use optionstratlib::model::types::{ExpirationDate, PZERO}; use optionstratlib::pos; @@ -63,6 +64,6 @@ fn main() -> Result<(), Box> { (1400, 933), )?; } - + info!("Greeks: {:#?}", strategy.greeks()); Ok(()) } 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 06460813..983cda69 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 @@ -1,5 +1,6 @@ use optionstratlib::chains::chain::OptionChain; use optionstratlib::constants::ZERO; +use optionstratlib::greeks::equations::Greeks; use optionstratlib::model::types::PositiveF64; use optionstratlib::model::types::{ExpirationDate, PZERO}; use optionstratlib::pos; @@ -64,6 +65,7 @@ fn main() -> Result<(), Box> { (1400, 933), )?; } + info!("Greeks: {:#?}", strategy.greeks()); Ok(()) } 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 a08ea823..04778a2c 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 @@ -1,5 +1,6 @@ use optionstratlib::chains::chain::OptionChain; use optionstratlib::constants::ZERO; +use optionstratlib::greeks::equations::Greeks; use optionstratlib::model::types::PositiveF64; use optionstratlib::model::types::{ExpirationDate, PZERO}; use optionstratlib::pos; @@ -65,5 +66,7 @@ fn main() -> Result<(), Box> { )?; } + info!("Greeks: {:#?}", strategy.greeks()); + Ok(()) } diff --git a/examples/examples_strategies_delta/src/bin/strategy_call_butterfly.rs b/examples/examples_strategies_delta/src/bin/strategy_call_butterfly.rs index 32d2b0ad..a07d5c65 100644 --- a/examples/examples_strategies_delta/src/bin/strategy_call_butterfly.rs +++ b/examples/examples_strategies_delta/src/bin/strategy_call_butterfly.rs @@ -24,7 +24,7 @@ fn main() -> Result<(), Box> { 0.05, // risk_free_rate 0.0, // dividend_yield pos!(1.0), // long quantity - pos!(2.0), // short_quantity + 97.8, // short_quantity 85.04, // premium_long_itm 31.65, // premium_long_otm 53.04, // premium_short @@ -32,6 +32,7 @@ fn main() -> Result<(), Box> { 0.78, // close_fee_long 0.73, // close_fee_short 0.73, // close_fee_short + 0.73, ); info!("Title: {}", strategy.title()); diff --git a/src/chains/chain.rs b/src/chains/chain.rs index ae8468d4..3eba4c34 100644 --- a/src/chains/chain.rs +++ b/src/chains/chain.rs @@ -5,13 +5,14 @@ ******************************************************************************/ use crate::chains::utils::{ adjust_volatility, default_empty_string, generate_list_of_strikes, parse, - OptionChainBuildParams, OptionDataPriceParams, RandomPositionsParams, + OptionChainBuildParams, OptionChainParams, OptionDataPriceParams, RandomPositionsParams, }; use crate::chains::{DeltasInStrike, OptionsInStrike}; +use crate::constants::ZERO; use crate::greeks::equations::delta; use crate::model::option::Options; use crate::model::position::Position; -use crate::model::types::{OptionStyle, OptionType, PositiveF64, Side, PZERO}; +use crate::model::types::{ExpirationDate, OptionStyle, OptionType, PositiveF64, Side, PZERO}; use crate::pricing::black_scholes_model::black_scholes; use crate::strategies::utils::FindOptimalSide; use crate::utils::others::get_random_element; @@ -331,15 +332,25 @@ pub struct OptionChain { pub underlying_price: PositiveF64, expiration_date: String, pub(crate) options: BTreeSet, + pub(crate) risk_free_rate: Option, + pub(crate) dividend_yield: Option, } impl OptionChain { - pub fn new(symbol: &str, underlying_price: PositiveF64, expiration_date: String) -> Self { + pub fn new( + symbol: &str, + underlying_price: PositiveF64, + expiration_date: String, + risk_free_rate: Option, + dividend_yield: Option, + ) -> Self { OptionChain { symbol: symbol.to_string(), underlying_price, expiration_date, options: BTreeSet::new(), + risk_free_rate, + dividend_yield, } } @@ -348,6 +359,8 @@ impl OptionChain { ¶ms.symbol, params.price_params.underlying_price, params.price_params.expiration_date.get_date_string(), + None, + None, ); let strikes = generate_list_of_strikes( @@ -548,6 +561,8 @@ impl OptionChain { underlying_price: PZERO, expiration_date: "unknown".to_string(), options, + risk_free_rate: None, + dividend_yield: None, }; option_chain.set_from_title(file_path); Ok(option_chain) @@ -720,6 +735,21 @@ impl OptionChain { Ok(positions) } + /// Returns an iterator over the `options` field in the `OptionChain` structure. + /// + /// This method provides a mechanism to traverse through the set of options + /// (`OptionData`) associated with an `OptionChain`. + /// + /// # Returns + /// + /// An iterator that yields references to the `OptionData` elements in the `options` field. + /// Since the `options` field is stored as a `BTreeSet`, the elements are ordered + /// in ascending order based on the sorting rules of `BTreeSet` (typically defined by `Ord` implementation). + /// + pub fn get_single_iter(&self) -> impl Iterator { + self.options.iter() + } + /// Returns an iterator that generates pairs of distinct option combinations from the `OptionChain`. /// /// This function iterates over all unique combinations of two options from the `options` collection @@ -733,12 +763,13 @@ impl OptionChain { /// # Example /// /// ```rust + /// use tracing::info; /// use optionstratlib::chains::chain::OptionChain; /// use optionstratlib::model::types::PositiveF64; /// use optionstratlib::pos; - /// let mut option_chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + /// let mut option_chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); /// for (option1, option2) in option_chain.get_double_iter() { - /// println!("{:?}, {:?}", option1, option2); + /// info!("{:?}, {:?}", option1, option2); /// } /// ``` pub fn get_double_iter(&self) -> impl Iterator { @@ -763,12 +794,13 @@ impl OptionChain { /// # Example /// /// ```rust + /// use tracing::info; /// use optionstratlib::chains::chain::OptionChain; /// use optionstratlib::model::types::PositiveF64; /// use optionstratlib::pos; - /// let mut option_chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + /// let mut option_chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); /// for (option1, option2) in option_chain.get_double_inclusive_iter() { - /// println!("{:?}, {:?}", option1, option2); + /// info!("{:?}, {:?}", option1, option2); /// } /// ``` pub fn get_double_inclusive_iter(&self) -> impl Iterator { @@ -790,12 +822,13 @@ impl OptionChain { /// # Example /// /// ```rust + /// use tracing::info; /// use optionstratlib::chains::chain::OptionChain; /// use optionstratlib::model::types::PositiveF64; /// use optionstratlib::pos; - /// let mut option_chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + /// let mut option_chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); /// for (option1, option2, option3) in option_chain.get_triple_iter() { - /// println!("{:?}, {:?}, {:?}", option1, option2, option3); + /// info!("{:?}, {:?}, {:?}", option1, option2, option3); /// } /// ``` pub fn get_triple_iter(&self) -> impl Iterator { @@ -826,12 +859,13 @@ impl OptionChain { /// # Example /// /// ```rust + /// use tracing::info; /// use optionstratlib::chains::chain::OptionChain; /// use optionstratlib::model::types::PositiveF64; /// use optionstratlib::pos; - /// let mut option_chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + /// let mut option_chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); /// for (option1, option2, option3) in option_chain.get_triple_inclusive_iter() { - /// println!("{:?}, {:?}, {:?}", option1, option2, option3); + /// info!("{:?}, {:?}, {:?}", option1, option2, option3); /// } /// ``` pub fn get_triple_inclusive_iter( @@ -863,12 +897,13 @@ impl OptionChain { /// # Example /// /// ```rust + /// use tracing::info; /// use optionstratlib::chains::chain::OptionChain; /// use optionstratlib::model::types::PositiveF64; /// use optionstratlib::pos; - /// let mut option_chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + /// let mut option_chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); /// for (option1, option2, option3, option4) in option_chain.get_quad_iter() { - /// println!("{:?}, {:?}, {:?}, {:?}", option1, option2, option3, option4); + /// info!("{:?}, {:?}, {:?}, {:?}", option1, option2, option3, option4); /// } /// ``` pub fn get_quad_iter( @@ -907,12 +942,13 @@ impl OptionChain { /// # Example /// /// ```rust + /// use tracing::info; /// use optionstratlib::chains::chain::OptionChain; /// use optionstratlib::model::types::PositiveF64; /// use optionstratlib::pos; - /// let mut option_chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + /// let mut option_chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); /// for (option1, option2, option3, option4) in option_chain.get_quad_inclusive_iter() { - /// println!("{:?}, {:?}, {:?}, {:?}", option1, option2, option3, option4); + /// info!("{:?}, {:?}, {:?}, {:?}", option1, option2, option3, option4); /// } /// ``` pub fn get_quad_inclusive_iter( @@ -939,6 +975,28 @@ impl OptionChain { } } +impl OptionChainParams for OptionChain { + fn get_params(&self, strike_price: PositiveF64) -> Result { + let option = self + .options + .iter() + .find(|option| option.strike_price == strike_price); + if option.is_none() { + return Err(format!( + "Option with strike price {} not found", + strike_price + )); + } + Ok(OptionDataPriceParams::new( + self.underlying_price, + ExpirationDate::from_string(&self.expiration_date)?, + option.unwrap().implied_volatility, + self.risk_free_rate.unwrap_or(ZERO), + self.dividend_yield.unwrap_or(ZERO), + )) + } +} + impl Display for OptionChain { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "Symbol: {}", self.symbol)?; @@ -986,7 +1044,13 @@ mod tests_chain_base { #[test] fn test_new_option_chain() { - let chain = OptionChain::new("SP500", pos!(5781.88), "18-oct-2024".to_string()); + let chain = OptionChain::new( + "SP500", + pos!(5781.88), + "18-oct-2024".to_string(), + None, + None, + ); assert_eq!(chain.symbol, "SP500"); assert_eq!(chain.underlying_price, 5781.88); assert_eq!(chain.expiration_date, "18-oct-2024"); @@ -1070,7 +1134,13 @@ mod tests_chain_base { #[test] fn test_add_option() { - let mut chain = OptionChain::new("SP500", pos!(5781.88), "18-oct-2024".to_string()); + let mut chain = OptionChain::new( + "SP500", + pos!(5781.88), + "18-oct-2024".to_string(), + None, + None, + ); chain.add_option( pos!(5520.0), spos!(274.26), @@ -1092,19 +1162,31 @@ mod tests_chain_base { #[test] fn test_get_title_i() { - let chain = OptionChain::new("SP500", pos!(5781.88), "18-oct-2024".to_string()); + let chain = OptionChain::new( + "SP500", + pos!(5781.88), + "18-oct-2024".to_string(), + None, + None, + ); assert_eq!(chain.get_title(), "SP500-18-oct-2024-5781.88"); } #[test] fn test_get_title_ii() { - let chain = OptionChain::new("SP500", pos!(5781.88), "18 oct 2024".to_string()); + let chain = OptionChain::new( + "SP500", + pos!(5781.88), + "18 oct 2024".to_string(), + None, + None, + ); assert_eq!(chain.get_title(), "SP500-18-oct-2024-5781.88"); } #[test] fn test_set_from_title_i() { - let mut chain = OptionChain::new("", PZERO, "".to_string()); + let mut chain = OptionChain::new("", PZERO, "".to_string(), None, None); chain.set_from_title("SP500-18-oct-2024-5781.88.csv"); assert_eq!(chain.symbol, "SP500"); assert_eq!(chain.expiration_date, "18-oct-2024"); @@ -1113,7 +1195,7 @@ mod tests_chain_base { #[test] fn test_set_from_title_ii() { - let mut chain = OptionChain::new("", PZERO, "".to_string()); + let mut chain = OptionChain::new("", PZERO, "".to_string(), None, None); chain.set_from_title("path/SP500-18-oct-2024-5781.88.csv"); assert_eq!(chain.symbol, "SP500"); assert_eq!(chain.expiration_date, "18-oct-2024"); @@ -1122,7 +1204,7 @@ mod tests_chain_base { #[test] fn test_set_from_title_iii() { - let mut chain = OptionChain::new("", PZERO, "".to_string()); + let mut chain = OptionChain::new("", PZERO, "".to_string(), None, None); chain.set_from_title("path/SP500-18-oct-2024-5781.csv"); assert_eq!(chain.symbol, "SP500"); assert_eq!(chain.expiration_date, "18-oct-2024"); @@ -1131,7 +1213,7 @@ mod tests_chain_base { #[test] fn test_set_from_title_iv() { - let mut chain = OptionChain::new("", PZERO, "".to_string()); + let mut chain = OptionChain::new("", PZERO, "".to_string(), None, None); chain.set_from_title("path/SP500-18-oct-2024-5781.88.json"); assert_eq!(chain.symbol, "SP500"); assert_eq!(chain.expiration_date, "18-oct-2024"); @@ -1140,7 +1222,7 @@ mod tests_chain_base { #[test] fn test_set_from_title_v() { - let mut chain = OptionChain::new("", PZERO, "".to_string()); + let mut chain = OptionChain::new("", PZERO, "".to_string(), None, None); chain.set_from_title("path/SP500-18-oct-2024-5781.json"); assert_eq!(chain.symbol, "SP500"); assert_eq!(chain.expiration_date, "18-oct-2024"); @@ -1149,7 +1231,13 @@ mod tests_chain_base { #[test] fn test_save_to_csv() { - let mut chain = OptionChain::new("SP500", pos!(5781.88), "18-oct-2024".to_string()); + let mut chain = OptionChain::new( + "SP500", + pos!(5781.88), + "18-oct-2024".to_string(), + None, + None, + ); chain.add_option( pos!(5520.0), spos!(274.26), @@ -1170,7 +1258,13 @@ mod tests_chain_base { #[test] fn test_save_to_json() { - let mut chain = OptionChain::new("SP500", pos!(5781.88), "18-oct-2024".to_string()); + let mut chain = OptionChain::new( + "SP500", + pos!(5781.88), + "18-oct-2024".to_string(), + None, + None, + ); chain.add_option( pos!(5520.0), spos!(274.26), @@ -1193,7 +1287,13 @@ mod tests_chain_base { #[test] fn test_load_from_csv() { setup_logger(); - let mut chain = OptionChain::new("SP500", pos!(5781.89), "18-oct-2024".to_string()); + let mut chain = OptionChain::new( + "SP500", + pos!(5781.89), + "18-oct-2024".to_string(), + None, + None, + ); chain.add_option( pos!(5520.0), spos!(274.26), @@ -1222,7 +1322,8 @@ mod tests_chain_base { #[test] fn test_load_from_json() { - let mut chain = OptionChain::new("SP500", pos!(5781.9), "18-oct-2024".to_string()); + let mut chain = + OptionChain::new("SP500", pos!(5781.9), "18-oct-2024".to_string(), None, None); chain.add_option( pos!(5520.0), spos!(274.26), @@ -1492,7 +1593,7 @@ mod tests_get_random_positions { fn create_test_chain() -> OptionChain { // Create a sample option chain - let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); // Add some test options with different strikes chain.add_option( @@ -1738,7 +1839,7 @@ mod tests_get_random_positions { #[test] fn test_empty_chain() { setup_logger(); - let chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + let chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); let params = RandomPositionsParams::new( Some(1), None, @@ -1872,7 +1973,7 @@ mod tests_filter_option_data { use crate::pos; fn create_test_chain() -> OptionChain { - let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); for strike in [90.0, 95.0, 100.0, 105.0, 110.0].iter() { chain.add_option( @@ -1935,13 +2036,13 @@ mod tests_strike_price_range_vec { #[test] fn test_empty_chain() { - let chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + let chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); assert_eq!(chain.strike_price_range_vec(5.0), None); } #[test] fn test_single_option() { - let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); chain.add_option( pos!(100.0), None, @@ -1960,7 +2061,7 @@ mod tests_strike_price_range_vec { #[test] fn test_multiple_options() { - let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); for strike in [90.0, 95.0, 100.0].iter() { chain.add_option( pos!(*strike), @@ -1980,7 +2081,7 @@ mod tests_strike_price_range_vec { #[test] fn test_step_size() { - let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); for strike in [90.0, 100.0].iter() { chain.add_option( pos!(*strike), @@ -2229,7 +2330,7 @@ mod tests_filter_options_in_strike { use crate::pos; fn create_test_chain() -> OptionChain { - let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); for strike in [90.0, 95.0, 100.0, 105.0, 110.0].iter() { chain.add_option( @@ -2342,7 +2443,7 @@ mod tests_filter_options_in_strike { #[test] fn test_filter_empty_chain() { - let chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + let chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); let price_params = OptionDataPriceParams::new( pos!(100.0), ExpirationDate::Days(30.0), @@ -2418,7 +2519,7 @@ mod tests_chain_iterators { use crate::spos; fn create_test_chain() -> OptionChain { - let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); // Add three options with different strikes chain.add_option( @@ -2462,14 +2563,14 @@ mod tests_chain_iterators { #[test] fn test_get_double_iter_empty() { - let chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + let chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); let pairs: Vec<_> = chain.get_double_iter().collect(); assert!(pairs.is_empty()); } #[test] fn test_get_double_iter_single() { - let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); chain.add_option( pos!(100.0), spos!(3.0), @@ -2507,14 +2608,14 @@ mod tests_chain_iterators { #[test] fn test_get_double_inclusive_iter_empty() { - let chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + let chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); let pairs: Vec<_> = chain.get_double_inclusive_iter().collect(); assert!(pairs.is_empty()); } #[test] fn test_get_double_inclusive_iter_single() { - let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); chain.add_option( pos!(100.0), spos!(3.0), @@ -2567,7 +2668,7 @@ mod tests_chain_iterators_bis { use crate::spos; fn create_test_chain() -> OptionChain { - let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); // Add four options with different strikes chain.add_option( @@ -2624,14 +2725,14 @@ mod tests_chain_iterators_bis { // Tests for Triple Iterator #[test] fn test_get_triple_iter_empty() { - let chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + let chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); let triples: Vec<_> = chain.get_triple_iter().collect(); assert!(triples.is_empty()); } #[test] fn test_get_triple_iter_two_elements() { - let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); // Add two options chain.add_option(pos!(90.0), None, None, None, None, None, None, None, None); chain.add_option(pos!(100.0), None, None, None, None, None, None, None, None); @@ -2662,14 +2763,14 @@ mod tests_chain_iterators_bis { // Tests for Triple Inclusive Iterator #[test] fn test_get_triple_inclusive_iter_empty() { - let chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + let chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); let triples: Vec<_> = chain.get_triple_inclusive_iter().collect(); assert!(triples.is_empty()); } #[test] fn test_get_triple_inclusive_iter_single() { - let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); chain.add_option(pos!(100.0), None, None, None, None, None, None, None, None); let triples: Vec<_> = chain.get_triple_inclusive_iter().collect(); @@ -2695,14 +2796,14 @@ mod tests_chain_iterators_bis { // Tests for Quad Iterator #[test] fn test_get_quad_iter_empty() { - let chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + let chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); let quads: Vec<_> = chain.get_quad_iter().collect(); assert!(quads.is_empty()); } #[test] fn test_get_quad_iter_three_elements() { - let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); // Add three options chain.add_option(pos!(90.0), None, None, None, None, None, None, None, None); chain.add_option(pos!(100.0), None, None, None, None, None, None, None, None); @@ -2730,14 +2831,14 @@ mod tests_chain_iterators_bis { // Tests for Quad Inclusive Iterator #[test] fn test_get_quad_inclusive_iter_empty() { - let chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + let chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); let quads: Vec<_> = chain.get_quad_inclusive_iter().collect(); assert!(quads.is_empty()); } #[test] fn test_get_quad_inclusive_iter_single() { - let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string()); + let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-01-01".to_string(), None, None); chain.add_option(pos!(100.0), None, None, None, None, None, None, None, None); let quads: Vec<_> = chain.get_quad_inclusive_iter().collect(); diff --git a/src/chains/mod.rs b/src/chains/mod.rs index 9540de0d..c6f94d68 100644 --- a/src/chains/mod.rs +++ b/src/chains/mod.rs @@ -35,7 +35,9 @@ //! let chain = OptionChain::new( //! "SP500", //! PositiveF64::new(100.0).unwrap(), -//! "2024-12-31".to_string() +//! "2024-12-31".to_string(), +//! None, +//! None //! ); //! //! // Build chain with specific parameters diff --git a/src/chains/utils.rs b/src/chains/utils.rs index 0bc5e188..ec29bf03 100644 --- a/src/chains/utils.rs +++ b/src/chains/utils.rs @@ -3,12 +3,26 @@ Email: jb@taunais.com Date: 25/10/24 ******************************************************************************/ +use crate::chains::chain::OptionData; use crate::constants::ZERO; use crate::model::types::{ExpirationDate, PositiveF64, PZERO}; use crate::pos; use std::collections::BTreeSet; use std::fmt::Display; +#[derive(Debug)] +pub enum OptionDataGroup<'a> { + One(&'a OptionData), + Two(&'a OptionData, &'a OptionData), + Three(&'a OptionData, &'a OptionData, &'a OptionData), + Four( + &'a OptionData, + &'a OptionData, + &'a OptionData, + &'a OptionData, + ), + Any(Vec<&'a OptionData>), +} pub struct OptionChainBuildParams { pub(crate) symbol: String, pub(crate) volume: Option, @@ -118,7 +132,7 @@ impl Display for OptionDataPriceParams { } pub trait OptionChainParams { - fn get_params(&self) -> Result; + fn get_params(&self, strike_price: PositiveF64) -> Result; } /// Parameters for generating random positions in an option chain diff --git a/src/greeks/equations.rs b/src/greeks/equations.rs index 89f5cd8c..d43eddd9 100644 --- a/src/greeks/equations.rs +++ b/src/greeks/equations.rs @@ -11,7 +11,7 @@ use crate::model::types::OptionStyle; use tracing::trace; #[allow(dead_code)] -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub struct Greek { pub delta: f64, pub gamma: f64, diff --git a/src/lib.rs b/src/lib.rs index 5af59ad0..1ddf5eab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,19 +6,23 @@ //! //!
-//! OptionStratLib +//! OptionStratLib //!
//! //! [![Dual License](https://img.shields.io/badge/license-MIT%20and%20Apache%202.0-blue)](./LICENSE) //! [![Crates.io](https://img.shields.io/crates/v/optionstratlib.svg)](https://crates.io/crates/optionstratlib) //! [![Downloads](https://img.shields.io/crates/d/optionstratlib.svg)](https://crates.io/crates/optionstratlib) //! [![Stars](https://img.shields.io/github/stars/joaquinbejar/OptionStratLib.svg)](https://github.com/joaquinbejar/OptionStratLib/stargazers) +//! [![Issues](https://img.shields.io/github/issues/joaquinbejar/OptionStratLib.svg)](https://github.com/joaquinbejar/OptionStratLib/issues) +//! [![PRs](https://img.shields.io/github/issues-pr/joaquinbejar/OptionStratLib.svg)](https://github.com/joaquinbejar/OptionStratLib/pulls) +//! //! //! [![Build Status](https://img.shields.io/github/workflow/status/joaquinbejar/OptionStratLib/CI)](https://github.com/joaquinbejar/OptionStratLib/actions) //! [![Coverage](https://img.shields.io/codecov/c/github/joaquinbejar/OptionStratLib)](https://codecov.io/gh/joaquinbejar/OptionStratLib) //! [![Dependencies](https://img.shields.io/librariesio/github/joaquinbejar/OptionStratLib)](https://libraries.io/github/joaquinbejar/OptionStratLib) +//! [![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://docs.rs/optionstratlib) //! -//! # OptionStratLib v0.2.4: Financial Options Library +//! # OptionStratLib v0.2.5: Financial Options Library //! //! ## Table of Contents //! 1. [Introduction](#introduction) diff --git a/src/model/mod.rs b/src/model/mod.rs index d2557b10..8ccbefd3 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -69,6 +69,7 @@ //! ## Example Usage //! //! ```rust +//! use tracing::info; //! use optionstratlib::model::option::Options; //! use optionstratlib::model::types::{ExpirationDate, OptionStyle, OptionType, Side}; //! use optionstratlib::pos; @@ -89,8 +90,8 @@ //! None, //! ); //! -//! println!("Option Details: {}", option); -//! println!("Debug View: {:?}", option); +//! info!("Option Details: {}", option); +//! info!("Debug View: {:?}", option); //! ``` mod format; diff --git a/src/model/types.rs b/src/model/types.rs index 60c451dd..1396b56a 100644 --- a/src/model/types.rs +++ b/src/model/types.rs @@ -10,6 +10,7 @@ use std::ops::{Add, AddAssign, Div, Mul, MulAssign, Neg, Sub}; use std::str::FromStr; pub const PZERO: PositiveF64 = PositiveF64(ZERO); +pub const INFINITY: PositiveF64 = PositiveF64(f64::INFINITY); pub const SIZE_ONE: PositiveF64 = PositiveF64(1.0); pub const P_INFINITY: PositiveF64 = PositiveF64(f64::INFINITY); @@ -375,6 +376,16 @@ impl ExpirationDate { let date = self.get_date(); date.format("%Y-%m-%d").to_string() } + + pub fn from_string(s: &String) -> Result { + if let Ok(days) = s.parse::() { + Ok(ExpirationDate::Days(days)) + } else if let Ok(datetime) = DateTime::parse_from_rfc3339(s) { + Ok(ExpirationDate::DateTime(DateTime::from(datetime))) + } else { + Err(format!("Failed to parse ExpirationDate from string: {}", s)) + } + } } impl Default for ExpirationDate { diff --git a/src/simulation/mod.rs b/src/simulation/mod.rs index c9d906cb..8a83902f 100644 --- a/src/simulation/mod.rs +++ b/src/simulation/mod.rs @@ -84,6 +84,7 @@ //! ### Using the Iterator Interface //! //! ```rust +//! use tracing::info; //! use optionstratlib::model::types::PositiveF64; //! use optionstratlib::utils::time::TimeFrame; //! use optionstratlib::pos; @@ -109,7 +110,7 @@ //! //! // Iterate through the price path //! for params in &mut walk { -//! println!( +//! info!( //! "Price: {}, Volatility: {:?}", //! params.get_underlying_price(), //! params.get_implied_volatility() diff --git a/src/strategies/base.rs b/src/strategies/base.rs index 16e0e13f..d3cc823a 100644 --- a/src/strategies/base.rs +++ b/src/strategies/base.rs @@ -5,6 +5,7 @@ ******************************************************************************/ use crate::chains::chain::{OptionChain, OptionData}; +use crate::chains::utils::OptionDataGroup; use crate::chains::StrategyLegs; use crate::constants::{ STRIKE_PRICE_LOWER_BOUND_MULTIPLIER, STRIKE_PRICE_UPPER_BOUND_MULTIPLIER, ZERO, @@ -14,6 +15,7 @@ use crate::model::types::{PositiveF64, PZERO}; use crate::strategies::utils::{calculate_price_range, FindOptimalSide, OptimizationCriteria}; use crate::{pos, spos}; use std::f64; +use tracing::error; /// This enum represents different types of trading strategies. /// Each variant represents a specific strategy type. @@ -237,6 +239,42 @@ pub trait Optimizable: Validable + Strategies { self.find_optimal(option_chain, side, OptimizationCriteria::Area); } + /// Filters and generates combinations of options data from the given `OptionChain`. + /// + /// # Parameters + /// - `&self`: A reference to the current object/context that holds the filtering logic or required data. + /// - `_option_chain`: A reference to an `OptionChain` object that contains relevant financial information + /// such as options data, underlying price, and expiration date. + /// - `_side`: A `FindOptimalSide` value that specifies the filtering strategy for finding combinations of + /// options. It can specify: + /// - `Upper`: Consider options higher than a certain threshold. + /// - `Lower`: Consider options lower than a certain threshold. + /// - `All`: Include all options. + /// - `Range(start, end)`: Consider options within a specified range. + /// + /// # Returns + /// - An iterator that yields `OptionDataGroup` items. These items represent combinations of options data filtered + /// based on the given criteria. The `OptionDataGroup` can represent combinations of 2, 3, 4, or any number + /// of options depending on the grouping logic. + /// + /// **Note**: + /// - The current implementation returns an empty iterator (`std::iter::empty()`) as a placeholder. + /// - You may modify this method to implement the actual filtering and combination logic based on the + /// provided `OptionChain` and `FindOptimalSide` criteria. + /// + /// # See Also + /// - `FindOptimalSide` for the strategy enumeration. + /// - `OptionDataGroup` for the structure of grouped combinations. + /// - `OptionChain` for the full structure being processed. + fn filter_combinations<'a>( + &'a self, + _option_chain: &'a OptionChain, + _side: FindOptimalSide, + ) -> impl Iterator> { + error!("Filter combinations is not applicable for this strategy"); + std::iter::empty() + } + fn find_optimal( &mut self, _option_chain: &OptionChain, diff --git a/src/strategies/bear_call_spread.rs b/src/strategies/bear_call_spread.rs index 1becc3c2..924deecf 100644 --- a/src/strategies/bear_call_spread.rs +++ b/src/strategies/bear_call_spread.rs @@ -29,7 +29,8 @@ Key characteristics: */ use super::base::{Optimizable, Positionable, Strategies, StrategyType, Validable}; -use crate::chains::chain::{OptionChain, OptionData}; +use crate::chains::chain::OptionChain; +use crate::chains::utils::OptionDataGroup; use crate::chains::StrategyLegs; use crate::constants::{DARK_BLUE, DARK_GREEN, ZERO}; use crate::greeks::equations::{Greek, Greeks}; @@ -269,72 +270,70 @@ impl Validable for BearCallSpread { impl Optimizable for BearCallSpread { type Strategy = BearCallSpread; + fn filter_combinations<'a>( + &'a self, + option_chain: &'a OptionChain, + side: FindOptimalSide, + ) -> impl Iterator> { + let underlying_price = self.get_underlying_price(); + let strategy = self.clone(); + option_chain + .get_double_iter() + // Filter out invalid combinations based on FindOptimalSide + .filter(move |&option| { + option.0.is_valid_optimal_side(underlying_price, &side) + && option.1.is_valid_optimal_side(underlying_price, &side) + }) + // Filter out options with invalid bid/ask prices + .filter(|(short, long)| { + long.call_ask.unwrap_or(PZERO) > PZERO && short.call_bid.unwrap_or(PZERO) > PZERO + }) + // Filter out options that don't meet strategy constraints + .filter(move |(short_option, long_option)| { + let legs = StrategyLegs::TwoLegs { + first: short_option, + second: long_option, + }; + let strategy = strategy.create_strategy(option_chain, &legs); + strategy.validate() && strategy.max_profit().is_ok() && strategy.max_loss().is_ok() + }) + // Map to OptionDataGroup + .map(move |(short, long)| OptionDataGroup::Two(short, long)) + } + fn find_optimal( &mut self, option_chain: &OptionChain, side: FindOptimalSide, criteria: OptimizationCriteria, ) { - let options: Vec<&OptionData> = option_chain.options.iter().collect(); let mut best_value = f64::NEG_INFINITY; - - for short_index in 0..options.len() { - let short_option = &options[short_index]; - - for long_option in &options[short_index + 1..] { - if !self.is_valid_short_option(short_option, &side) - || !self.is_valid_long_option(long_option, &side) - { - debug!( - "Invalid options Asset {} - Short({}) Long({})", - option_chain.underlying_price, - short_option.strike_price, - long_option.strike_price, - ); - continue; - } - - let legs = StrategyLegs::TwoLegs { - first: short_option, - second: long_option, - }; - - if !self.are_valid_prices(&legs) { - debug!( - "Invalid prices - Short({}): {:?} Long({}): {:?}", - short_option.strike_price, - short_option.call_bid, - long_option.strike_price, - long_option.call_ask - ); - continue; - } - - let strategy = self.create_strategy(option_chain, &legs); - - if !strategy.validate() { - debug!("Invalid strategy"); - continue; - } - - if strategy.max_profit().is_err() || strategy.max_loss().is_err() { - debug!( - "Invalid profit {} loss {}", - strategy.max_profit().unwrap_or(PZERO), - strategy.max_loss().unwrap_or(PZERO) - ); - continue; - } - - let current_value = match criteria { - OptimizationCriteria::Ratio => strategy.profit_ratio(), - OptimizationCriteria::Area => strategy.profit_area(), - }; - - if current_value > best_value { - best_value = current_value; - *self = strategy.clone(); - } + let strategy_clone = self.clone(); + let options_iter = strategy_clone.filter_combinations(option_chain, side); + + for option_data_group in options_iter { + // Unpack the OptionDataGroup into individual options + let (short_option, long_option) = match option_data_group { + OptionDataGroup::Two(first, second) => (first, second), + _ => panic!("Invalid OptionDataGroup"), + }; + + let legs = StrategyLegs::TwoLegs { + first: short_option, + second: long_option, + }; + let strategy = self.create_strategy(option_chain, &legs); + // Calculate the current value based on the optimization criteria + let current_value = match criteria { + OptimizationCriteria::Ratio => strategy.profit_ratio(), + OptimizationCriteria::Area => strategy.profit_area(), + }; + + if current_value > best_value { + // Update the best value and replace the current strategy + debug!("Found better value: {}", current_value); + best_value = current_value; + *self = strategy.clone(); } } } @@ -561,6 +560,1043 @@ impl DeltaNeutrality for BearCallSpread { } } +#[cfg(test)] +mod tests_bear_call_spread_strategies { + use super::*; + use crate::model::types::ExpirationDate; + use approx::assert_relative_eq; + + fn create_test_spread() -> BearCallSpread { + BearCallSpread::new( + "TEST".to_string(), + pos!(100.0), // underlying_price + pos!(95.0), // short_strike + pos!(105.0), // long_strike + ExpirationDate::Days(30.0), // expiration + 0.20, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(1.0), // quantity + 10.0, // premium_short_call + 5.0, // premium_long_call + 0.5, // open_fee_short_call + 0.5, // close_fee_short_call + 0.5, // open_fee_long_call + 0.5, // close_fee_long_call + ) + } + + #[test] + fn test_get_underlying_price() { + let spread = create_test_spread(); + assert_eq!(spread.get_underlying_price(), pos!(100.0)); + } + + #[test] + fn test_max_profit_positive() { + let spread = create_test_spread(); + let result = spread.max_profit(); + assert!(result.is_ok()); + assert_relative_eq!( + result.unwrap().value(), + spread.net_premium_received(), + epsilon = 0.0001 + ); + } + + #[test] + fn test_max_profit_negative() { + let mut spread = create_test_spread(); + // Modify premiums to create negative net premium + spread.short_call.premium = 1.0; + spread.long_call.premium = 2.0; + + let result = spread.max_profit(); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), "Net premium received is negative"); + } + + #[test] + fn test_max_loss() { + let spread = create_test_spread(); + let result = spread.max_loss(); + assert!(result.is_ok()); + + let width = + (spread.long_call.option.strike_price - spread.short_call.option.strike_price).value(); + let expected_loss = + width * spread.short_call.option.quantity.value() - spread.net_premium_received(); + assert_relative_eq!(result.unwrap().value(), expected_loss, epsilon = 0.0001); + } + + #[test] + fn test_max_loss_negative() { + let mut spread = create_test_spread(); + // Modify strikes to create invalid width + spread.short_call.option.strike_price = pos!(105.0); + spread.long_call.option.strike_price = pos!(95.0); + + let result = spread.max_loss(); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), "Max loss is negative"); + } + + #[test] + fn test_total_cost() { + let spread = create_test_spread(); + let expected_cost = spread.short_call.net_cost() + spread.long_call.net_cost(); + assert_relative_eq!(spread.total_cost().value(), expected_cost, epsilon = 0.0001); + } + + #[test] + fn test_net_premium_received() { + let spread = create_test_spread(); + let expected_premium = + spread.short_call.net_premium_received() - spread.long_call.net_cost(); + assert_relative_eq!( + spread.net_premium_received(), + expected_premium, + epsilon = 0.0001 + ); + } + + #[test] + fn test_fees() { + let spread = create_test_spread(); + let expected_fees = spread.short_call.open_fee + + spread.short_call.close_fee + + spread.long_call.open_fee + + spread.long_call.close_fee; + assert_relative_eq!(spread.fees(), expected_fees, epsilon = 0.0001); + } + + #[test] + fn test_profit_area() { + let spread = create_test_spread(); + let high = spread.max_profit().unwrap_or(PZERO); + let base = spread.break_even_points[0] - spread.short_call.option.strike_price; + let expected_area = (high * base / 200.0).value(); + assert_relative_eq!(spread.profit_area(), expected_area, epsilon = 0.0001); + } + + #[test] + fn test_profit_ratio_normal() { + let spread = create_test_spread(); + let max_profit = spread.max_profit().unwrap(); + let max_loss = spread.max_loss().unwrap(); + let expected_ratio = (max_profit / max_loss * 100.0).value(); + assert_relative_eq!(spread.profit_ratio(), expected_ratio, epsilon = 0.0001); + } + + #[test] + fn test_profit_ratio_zero_profit() { + let mut spread = create_test_spread(); + // Modify premiums to create zero max profit + spread.short_call.premium = 1.0; + spread.long_call.premium = 1.0; + + assert_relative_eq!(spread.profit_ratio(), 0.0, epsilon = 0.0001); + } + + #[test] + fn test_profit_ratio_zero_loss() { + let mut spread = create_test_spread(); + // Modify strikes to create zero max loss scenario + spread.long_call.option.strike_price = spread.short_call.option.strike_price; + + assert!(spread.profit_ratio().is_infinite()); + } + + #[test] + fn test_get_break_even_points() { + let spread = create_test_spread(); + let break_even_points = spread.get_break_even_points(); + assert!(!break_even_points.is_empty()); + assert_eq!(break_even_points.len(), 1); + + // Break even should be short strike plus net premium received per contract + let expected_break_even = spread.short_call.option.strike_price + + pos!(spread.net_premium_received() / spread.short_call.option.quantity.value()); + assert_relative_eq!( + break_even_points[0].value(), + expected_break_even.value(), + epsilon = 0.0001 + ); + } + + #[test] + #[should_panic] + fn test_with_different_quantities() { + let spread = BearCallSpread::new( + "TEST".to_string(), + pos!(100.0), + pos!(95.0), + pos!(105.0), + ExpirationDate::Days(30.0), + 0.20, + 0.05, + 0.0, + pos!(2.0), // quantity = 2 + 2.0, + 1.0, + 0.5, + 0.5, + 0.5, + 0.5, + ); + + // Check that all calculations scale properly with quantity + let base_spread = create_test_spread(); + assert_relative_eq!( + spread.max_profit().unwrap().value(), + base_spread.max_profit().unwrap().value() * 2.0, + epsilon = 0.0001 + ); + assert_relative_eq!( + spread.max_loss().unwrap().value(), + base_spread.max_loss().unwrap().value() * 2.0, + epsilon = 0.0001 + ); + } + + #[test] + fn test_with_different_strikes() { + let spread = BearCallSpread::new( + "TEST".to_string(), + pos!(100.0), + pos!(90.0), // wider spread + pos!(110.0), // wider spread + ExpirationDate::Days(30.0), + 0.20, + 0.05, + 0.0, + pos!(1.0), + 2.0, + 1.0, + 0.5, + 0.5, + 0.5, + 0.5, + ); + + // Check that strike width affects max loss calculation + let base_spread = create_test_spread(); + assert!(spread.max_loss().unwrap() > base_spread.max_loss().unwrap()); + } +} + +#[cfg(test)] +mod tests_bear_call_spread_positionable { + use super::*; + use crate::model::option::Options; + use crate::model::position::Position; + use crate::model::types::{ExpirationDate, OptionStyle}; + use chrono::Utc; + + // Helper function to create a test option + fn create_test_option(side: Side) -> Options { + Options::new( + OptionType::European, + side, + "TEST".to_string(), + pos!(100.0), + ExpirationDate::Days(30.0), + 0.2, + pos!(1.0), + pos!(100.0), + 0.05, + OptionStyle::Call, + 0.0, + None, + ) + } + + // Helper function to create a test position + fn create_test_position(side: Side) -> Position { + Position::new( + create_test_option(side), + 1.0, // premium + Utc::now(), // timestamp + 0.0, // open_fee + 0.0, // close_fee + ) + } + + #[test] + fn test_add_short_position() { + let mut spread = BearCallSpread::new( + "TEST".to_string(), + pos!(100.0), + pos!(95.0), + pos!(105.0), + ExpirationDate::Days(30.0), + 0.2, + 0.05, + 0.0, + pos!(1.0), + 2.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + ); + + let short_position = create_test_position(Side::Short); + let result = spread.add_position(&short_position); + + assert!(result.is_ok()); + assert_eq!(spread.short_call.option.side, Side::Short); + } + + #[test] + fn test_add_long_position() { + let mut spread = BearCallSpread::new( + "TEST".to_string(), + pos!(100.0), + pos!(95.0), + pos!(105.0), + ExpirationDate::Days(30.0), + 0.2, + 0.05, + 0.0, + pos!(1.0), + 2.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + ); + + let long_position = create_test_position(Side::Long); + let result = spread.add_position(&long_position); + + assert!(result.is_ok()); + assert_eq!(spread.long_call.option.side, Side::Long); + } + + #[test] + fn test_get_positions() { + let spread = BearCallSpread::new( + "TEST".to_string(), + pos!(100.0), + pos!(95.0), + pos!(105.0), + ExpirationDate::Days(30.0), + 0.2, + 0.05, + 0.0, + pos!(1.0), + 2.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + ); + + let result = spread.get_positions(); + assert!(result.is_ok()); + + let positions = result.unwrap(); + assert_eq!(positions.len(), 2); + assert_eq!(positions[0].option.side, Side::Short); + assert_eq!(positions[1].option.side, Side::Long); + } + + #[test] + fn test_add_multiple_positions() { + let mut spread = BearCallSpread::new( + "TEST".to_string(), + pos!(100.0), + pos!(95.0), + pos!(105.0), + ExpirationDate::Days(30.0), + 0.2, + 0.05, + 0.0, + pos!(1.0), + 2.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + ); + + let short_position = create_test_position(Side::Short); + let long_position = create_test_position(Side::Long); + + assert!(spread.add_position(&short_position).is_ok()); + assert!(spread.add_position(&long_position).is_ok()); + + let positions = spread.get_positions().unwrap(); + assert_eq!(positions.len(), 2); + } + + #[test] + fn test_replace_positions() { + let mut spread = BearCallSpread::new( + "TEST".to_string(), + pos!(100.0), + pos!(95.0), + pos!(105.0), + ExpirationDate::Days(30.0), + 0.2, + 0.05, + 0.0, + pos!(1.0), + 2.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + ); + + // Create new positions + let new_short = create_test_position(Side::Short); + let new_long = create_test_position(Side::Long); + + // Replace positions + assert!(spread.add_position(&new_short).is_ok()); + assert!(spread.add_position(&new_long).is_ok()); + } + + #[test] + fn test_positions_integrity() { + let mut spread = BearCallSpread::new( + "TEST".to_string(), + pos!(100.0), + pos!(95.0), + pos!(105.0), + ExpirationDate::Days(30.0), + 0.2, + 0.05, + 0.0, + pos!(1.0), + 2.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + ); + + let short_position = create_test_position(Side::Short); + let long_position = create_test_position(Side::Long); + + spread.add_position(&short_position).unwrap(); + spread.add_position(&long_position).unwrap(); + + let positions = spread.get_positions().unwrap(); + + // Verify position integrity + assert_eq!(positions[0].option.side, Side::Short); + assert_eq!(positions[1].option.side, Side::Long); + assert_eq!(positions[0].premium, 1.0); + assert_eq!(positions[1].premium, 1.0); + assert_eq!(positions[0].open_fee, 0.0); + assert_eq!(positions[1].open_fee, 0.0); + } +} +#[cfg(test)] +mod tests_bear_call_spread_validable { + use super::*; + use crate::model::types::ExpirationDate; + use crate::pos; + + fn create_valid_spread() -> BearCallSpread { + BearCallSpread::new( + "TEST".to_string(), + pos!(100.0), // underlying_price + pos!(95.0), // short_strike + pos!(105.0), // long_strike + ExpirationDate::Days(30.0), // expiration + 0.20, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(1.0), // quantity + 2.0, // premium_short_call + 1.0, // premium_long_call + 0.0, // fees + 0.0, + 0.0, + 0.0, + ) + } + + #[test] + fn test_valid_spread() { + let spread = create_valid_spread(); + assert!(spread.validate()); + } + + #[test] + fn test_invalid_strike_order() { + let spread = BearCallSpread::new( + "TEST".to_string(), + pos!(100.0), + pos!(105.0), // short strike higher than long strike + pos!(95.0), // long strike lower than short strike + ExpirationDate::Days(30.0), + 0.20, + 0.05, + 0.0, + pos!(1.0), + 2.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + ); + assert!(!spread.validate()); + } + + #[test] + fn test_equal_strikes() { + let spread = BearCallSpread::new( + "TEST".to_string(), + pos!(100.0), + pos!(100.0), // both strikes equal + pos!(100.0), // both strikes equal + ExpirationDate::Days(30.0), + 0.20, + 0.05, + 0.0, + pos!(1.0), + 2.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + ); + assert!(!spread.validate()); + } + + #[test] + fn test_invalid_short_call() { + let mut spread = create_valid_spread(); + // Invalidate short call by setting an invalid quantity + spread.short_call.option.quantity = pos!(0.0); + assert!(!spread.validate()); + } + + #[test] + fn test_invalid_long_call() { + let mut spread = create_valid_spread(); + // Invalidate long call by setting an invalid quantity + spread.long_call.option.quantity = pos!(0.0); + assert!(!spread.validate()); + } + + #[test] + #[should_panic] + fn test_invalid_expiration_dates() { + let spread = BearCallSpread::new( + "TEST".to_string(), + pos!(100.0), + pos!(95.0), + pos!(105.0), + ExpirationDate::Days(0.0), // Invalid expiration (0 days) + 0.20, + 0.05, + 0.0, + pos!(1.0), + 2.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + ); + assert!(!spread.validate()); + } + + #[test] + fn test_invalid_volatility() { + let spread = BearCallSpread::new( + "TEST".to_string(), + pos!(100.0), + pos!(95.0), + pos!(105.0), + ExpirationDate::Days(30.0), + -0.20, // Invalid negative volatility + 0.05, + 0.0, + pos!(1.0), + 2.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + ); + assert!(!spread.validate()); + } + + #[test] + fn test_invalid_underlying_price() { + let spread = BearCallSpread::new( + "TEST".to_string(), + pos!(0.0), // Invalid underlying price + pos!(95.0), + pos!(105.0), + ExpirationDate::Days(30.0), + 0.20, + 0.05, + 0.0, + pos!(1.0), + 2.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + ); + assert!(!spread.validate()); + } + + #[test] + fn test_strikes_too_close() { + let spread = BearCallSpread::new( + "TEST".to_string(), + pos!(100.0), + pos!(99.999), // Strikes very close to each other + pos!(100.001), // but technically different + ExpirationDate::Days(30.0), + 0.20, + 0.05, + 0.0, + pos!(1.0), + 2.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + ); + // Should still be valid as long as strikes are different + assert!(spread.validate()); + } + + #[test] + fn test_validation_with_different_quantities() { + let spread = BearCallSpread::new( + "TEST".to_string(), + pos!(100.0), + pos!(95.0), + pos!(105.0), + ExpirationDate::Days(30.0), + 0.20, + 0.05, + 0.0, + pos!(2.0), // Different quantity + 2.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + ); + // Should be valid as quantity > 0 + assert!(spread.validate()); + } +} +#[cfg(test)] +mod tests_bear_call_spread_profit { + use super::*; + use crate::model::types::ExpirationDate; + use crate::pricing::payoff::Profit; + use approx::assert_relative_eq; + + fn create_test_spread() -> BearCallSpread { + BearCallSpread::new( + "TEST".to_string(), + pos!(100.0), // underlying_price + pos!(95.0), // short_strike + pos!(105.0), // long_strike + ExpirationDate::Days(30.0), // expiration + 0.20, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(1.0), // quantity + 2.0, // premium_short_call + 1.0, // premium_long_call + 0.0, // open_fee_short_call + 0.0, // close_fee_short_call + 0.0, // open_fee_long_call + 0.0, // close_fee_long_call + ) + } + + #[test] + fn test_profit_below_short_strike() { + let spread = create_test_spread(); + let profit = spread.calculate_profit_at(pos!(90.0)); + // When price is below short strike, both options expire worthless + // Profit should be the net premium received + let expected_profit = spread.net_premium_received(); + assert_relative_eq!(profit, expected_profit, epsilon = 0.0001); + } + + #[test] + fn test_profit_at_short_strike() { + let spread = create_test_spread(); + let profit = spread.calculate_profit_at(pos!(95.0)); + // At short strike, short call is at-the-money + let expected_profit = spread.net_premium_received(); + assert_relative_eq!(profit, expected_profit, epsilon = 0.0001); + } + + #[test] + fn test_profit_between_strikes() { + let spread = create_test_spread(); + let test_price = pos!(100.0); + let profit = spread.calculate_profit_at(test_price); + // Between strikes, only short call is in-the-money + let intrinsic_value = test_price - spread.short_call.option.strike_price; + let expected_profit = spread.net_premium_received() - intrinsic_value.value(); + assert_relative_eq!(profit, expected_profit, epsilon = 0.0001); + } + + #[test] + fn test_profit_at_long_strike() { + let spread = create_test_spread(); + let profit = spread.calculate_profit_at(pos!(105.0)); + // At long strike, both options are in-the-money + let short_intrinsic = pos!(105.0) - spread.short_call.option.strike_price; + let long_intrinsic = pos!(105.0) - spread.long_call.option.strike_price; + let expected_profit = + spread.net_premium_received() - short_intrinsic.value() + long_intrinsic.value(); + assert_relative_eq!(profit, expected_profit, epsilon = 0.0001); + } + + #[test] + fn test_profit_above_long_strike() { + let spread = create_test_spread(); + let profit = spread.calculate_profit_at(pos!(110.0)); + // Maximum loss occurs when price is above long strike + let expected_profit = -spread.max_loss().unwrap().value(); + assert_relative_eq!(profit, expected_profit, epsilon = 0.0001); + } + + #[test] + fn test_profit_at_break_even() { + let spread = create_test_spread(); + let break_even = spread.get_break_even_points()[0]; + let profit = spread.calculate_profit_at(break_even); + // At break-even point, profit should be zero + assert_relative_eq!(profit, 0.0, epsilon = 0.0001); + } + + #[test] + fn test_profit_with_different_quantities() { + let spread = BearCallSpread::new( + "TEST".to_string(), + pos!(100.0), + pos!(95.0), + pos!(105.0), + ExpirationDate::Days(30.0), + 0.20, + 0.05, + 0.0, + pos!(2.0), // quantity = 2 + 2.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + ); + + let profit = spread.calculate_profit_at(pos!(90.0)); + // With quantity = 2, profit should be double + let expected_profit = spread.net_premium_received(); + assert_relative_eq!(profit, expected_profit, epsilon = 0.0001); + assert_relative_eq!( + profit, + 2.0 * create_test_spread().calculate_profit_at(pos!(90.0)), + epsilon = 0.0001 + ); + } + + #[test] + fn test_profit_with_fees() { + let spread = BearCallSpread::new( + "TEST".to_string(), + pos!(100.0), + pos!(95.0), + pos!(105.0), + ExpirationDate::Days(30.0), + 0.20, + 0.05, + 0.0, + pos!(1.0), + 2.0, + 1.0, + 0.5, // open_fee_short_call + 0.5, // close_fee_short_call + 0.5, // open_fee_long_call + 0.5, // close_fee_long_call + ); + + let profit = spread.calculate_profit_at(pos!(90.0)); + // Net premium should be reduced by total fees + let expected_profit = spread.net_premium_received(); + assert_relative_eq!(profit, expected_profit, epsilon = 0.0001); + assert!(profit < create_test_spread().calculate_profit_at(pos!(90.0))); + } +} + +#[cfg(test)] +mod tests_bear_call_spread_optimizable { + use super::*; + use crate::model::types::{ExpirationDate, PositiveF64}; + use crate::spos; + use crate::strategies::utils::{FindOptimalSide, OptimizationCriteria}; + + // Helper function to create a mock OptionChain for testing + fn create_mock_option_chain() -> OptionChain { + let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-03-15".to_string(), None, None); + + // Add options with different strikes and prices + chain.add_option( + pos!(95.0), // strike + spos!(6.0), // call_bid + spos!(6.2), // call_ask + spos!(1.0), // put_bid + spos!(1.2), // put_ask + spos!(0.2), // implied_vol + Some(0.7), // delta + spos!(100.0), // volume + Some(50), // open_interest + ); + + chain.add_option( + pos!(100.0), + spos!(3.0), + spos!(3.2), + spos!(3.0), + spos!(3.2), + spos!(0.2), + Some(0.5), + spos!(200.0), + Some(100), + ); + + chain.add_option( + pos!(105.0), + spos!(1.0), + spos!(1.2), + spos!(6.0), + spos!(6.2), + spos!(0.2), + Some(0.3), + spos!(150.0), + Some(75), + ); + + chain + } + + // Helper function to create a basic BearCallSpread for testing + fn create_test_strategy() -> BearCallSpread { + BearCallSpread::new( + "TEST".to_string(), + pos!(100.0), + pos!(95.0), + pos!(105.0), + ExpirationDate::Days(30.0), + 0.2, + 0.05, + 0.0, + pos!(1.0), + 3.0, + 1.2, + 0.0, + 0.0, + 0.0, + 0.0, + ) + } + + #[test] + fn test_filter_combinations_valid() { + let strategy = create_test_strategy(); + let chain = create_mock_option_chain(); + let combinations: Vec<_> = strategy + .filter_combinations(&chain, FindOptimalSide::Upper) + .collect(); + + assert!(!combinations.is_empty()); + + // Test some properties of the filtered combinations + for combination in combinations { + match combination { + OptionDataGroup::Two(short, long) => { + // Short strike should be lower than long strike + assert!(short.strike_price < long.strike_price); + + // Both options should have valid prices + assert!(short.call_bid.is_some()); + assert!(long.call_ask.is_some()); + + // Both options should have valid implied volatility + assert!(short.implied_volatility.is_some()); + assert!(long.implied_volatility.is_some()); + } + _ => panic!("Expected Two-leg combination"), + } + } + } + + #[test] + fn test_find_optimal_ratio() { + let mut strategy = create_test_strategy(); + let chain = create_mock_option_chain(); + + strategy.find_optimal(&chain, FindOptimalSide::All, OptimizationCriteria::Ratio); + + // Verify the strategy was updated with optimal values + assert!(strategy.validate()); + assert!(strategy.max_profit().is_ok()); + assert!(strategy.max_loss().is_ok()); + assert!(strategy.profit_ratio() > 0.0); + } + + #[test] + fn test_find_optimal_area() { + let mut strategy = create_test_strategy(); + let chain = create_mock_option_chain(); + + strategy.find_optimal(&chain, FindOptimalSide::All, OptimizationCriteria::Area); + + // Verify the strategy was updated with optimal values + assert!(strategy.validate()); + assert!(strategy.max_profit().is_ok()); + assert!(strategy.max_loss().is_ok()); + assert!(strategy.profit_area() > 0.0); + } + + #[test] + fn test_create_strategy() { + let strategy = create_test_strategy(); + let chain = create_mock_option_chain(); + + // Get two option data entries from the chain + let short_option = chain.options.iter().next().unwrap(); + let long_option = chain.options.iter().last().unwrap(); + + let legs = StrategyLegs::TwoLegs { + first: short_option, + second: long_option, + }; + + let new_strategy = strategy.create_strategy(&chain, &legs); + + // Verify the new strategy + assert!(new_strategy.validate()); + assert_eq!( + new_strategy.short_call.option.strike_price, + short_option.strike_price + ); + assert_eq!( + new_strategy.long_call.option.strike_price, + long_option.strike_price + ); + assert!(new_strategy.max_profit().is_ok()); + assert!(new_strategy.max_loss().is_ok()); + } + + #[test] + fn test_filter_combinations_empty_chain() { + let strategy = create_test_strategy(); + let empty_chain = + OptionChain::new("TEST", pos!(100.0), "2024-03-15".to_string(), None, None); + let combinations: Vec<_> = strategy + .filter_combinations(&empty_chain, FindOptimalSide::All) + .collect(); + + assert!(combinations.is_empty()); + } + + #[test] + fn test_filter_combinations_invalid_prices() { + let mut chain = create_mock_option_chain(); + // Add an option with invalid prices + chain.add_option( + pos!(110.0), + None, // Invalid call_bid + None, // Invalid call_ask + spos!(1.0), + spos!(1.2), + spos!(0.2), + Some(0.1), + spos!(50.0), + Some(25), + ); + + let strategy = create_test_strategy(); + let combinations: Vec<_> = strategy + .filter_combinations(&chain, FindOptimalSide::All) + .collect(); + + // Verify that invalid options are filtered out + for combination in combinations { + match combination { + OptionDataGroup::Two(short, long) => { + assert!(short.call_bid.is_some()); + assert!(long.call_ask.is_some()); + } + _ => panic!("Expected Two-leg combination"), + } + } + } + + #[test] + fn test_find_optimal_no_valid_combinations() { + let mut strategy = create_test_strategy(); + let mut empty_chain = + OptionChain::new("TEST", pos!(100.0), "2024-03-15".to_string(), None, None); + // Add invalid options + empty_chain.add_option(pos!(95.0), None, None, None, None, None, None, None, None); + + // Should not panic when no valid combinations exist + strategy.find_optimal( + &empty_chain, + FindOptimalSide::All, + OptimizationCriteria::Ratio, + ); + + // Strategy should remain unchanged + assert!(strategy.validate()); + } + + #[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 + let result = std::panic::catch_unwind(|| { + strategy.create_strategy( + &chain, + &StrategyLegs::TwoLegs { + first: chain.options.iter().next().unwrap(), + second: chain.options.iter().next().unwrap(), + }, + ); + }); + + assert!(result.is_err()); + } +} + #[cfg(test)] mod tests_bear_call_spread_graph { use super::*; diff --git a/src/strategies/bear_put_spread.rs b/src/strategies/bear_put_spread.rs index a350fe4e..702fe78c 100644 --- a/src/strategies/bear_put_spread.rs +++ b/src/strategies/bear_put_spread.rs @@ -15,7 +15,8 @@ Key characteristics: */ use super::base::{Optimizable, Positionable, Strategies, StrategyType, Validable}; -use crate::chains::chain::{OptionChain, OptionData}; +use crate::chains::chain::OptionChain; +use crate::chains::utils::OptionDataGroup; use crate::chains::StrategyLegs; use crate::constants::{DARK_BLUE, DARK_GREEN, ZERO}; use crate::greeks::equations::{Greek, Greeks}; @@ -37,7 +38,7 @@ use crate::visualization::utils::Graph; use chrono::Utc; use plotters::prelude::full_palette::ORANGE; use plotters::prelude::{ShapeStyle, RED}; -use tracing::debug; +use tracing::{debug, info}; const BEAR_PUT_SPREAD_DESCRIPTION: &str = "A bear put spread is created by buying a put option with a higher strike price \ @@ -243,7 +244,10 @@ impl Validable for BearPutSpread { return false; } if self.long_put.option.strike_price <= self.short_put.option.strike_price { - debug!("Long put strike price must be higher than short put strike price"); + debug!( + "Long put strike price {} must be higher than short put strike price {}", + self.long_put.option.strike_price, self.short_put.option.strike_price + ); return false; } true @@ -253,86 +257,76 @@ impl Validable for BearPutSpread { impl Optimizable for BearPutSpread { type Strategy = BearPutSpread; + fn filter_combinations<'a>( + &'a self, + option_chain: &'a OptionChain, + side: FindOptimalSide, + ) -> impl Iterator> { + let underlying_price = self.get_underlying_price(); + let strategy = self.clone(); + option_chain + .get_double_iter() + // Filter out invalid combinations based on FindOptimalSide + .filter(move |&option| { + option.0.is_valid_optimal_side(underlying_price, &side) + && option.1.is_valid_optimal_side(underlying_price, &side) + }) + // Filter out options with invalid bid/ask prices + .filter(|(short, long)| { + long.put_ask.unwrap_or(PZERO) > PZERO && short.put_bid.unwrap_or(PZERO) > PZERO + }) + // Filter out options that don't meet strategy constraints + .filter(move |(short, long)| { + let legs = StrategyLegs::TwoLegs { + first: short, + second: long, + }; + let strategy = strategy.create_strategy(option_chain, &legs); + strategy.validate() && strategy.max_profit().is_ok() && strategy.max_loss().is_ok() + }) + // Map to OptionDataGroup + .map(move |(short, long)| OptionDataGroup::Two(short, long)) + } + fn find_optimal( &mut self, option_chain: &OptionChain, side: FindOptimalSide, criteria: OptimizationCriteria, ) { - let options: Vec<&OptionData> = option_chain.options.iter().collect(); let mut best_value = f64::NEG_INFINITY; - - for short_index in 0..options.len() { - let short_option = &options[short_index]; - - for long_option in &options[short_index + 1..] { - if !self.is_valid_short_option(short_option, &side) - || !self.is_valid_long_option(long_option, &side) - { - debug!( - "Invalid options Asset {} - Long({}) Short({})", - option_chain.underlying_price, - long_option.strike_price, - short_option.strike_price, - ); - continue; - } - - let legs = StrategyLegs::TwoLegs { - first: long_option, - second: short_option, - }; - - if !self.are_valid_prices(&legs) { - debug!( - "Invalid prices - Long({}): {:?} Short({}): {:?}", - long_option.strike_price, - long_option.put_ask, - short_option.strike_price, - short_option.put_bid - ); - continue; - } - - let strategy = self.create_strategy(option_chain, &legs); - - if !strategy.validate() { - debug!("Invalid strategy"); - continue; - } - - if strategy.max_profit().is_err() || strategy.max_loss().is_err() { - debug!( - "Invalid profit {} loss {}", - strategy.max_profit().unwrap_or(PZERO), - strategy.max_loss().unwrap_or(PZERO) - ); - continue; - } - - let current_value = match criteria { - OptimizationCriteria::Ratio => strategy.profit_ratio(), - OptimizationCriteria::Area => strategy.profit_area(), - }; - - if current_value > best_value { - best_value = current_value; - *self = strategy.clone(); - } + let strategy_clone = self.clone(); + let options_iter = strategy_clone.filter_combinations(option_chain, side); + + for option_data_group in options_iter { + // Unpack the OptionDataGroup into individual options + let (short, long) = match option_data_group { + OptionDataGroup::Two(first, second) => (first, second), + _ => panic!("Invalid OptionDataGroup"), + }; + + let legs = StrategyLegs::TwoLegs { + first: short, + second: long, + }; + let strategy = self.create_strategy(option_chain, &legs); + // Calculate the current value based on the optimization criteria + let current_value = match criteria { + OptimizationCriteria::Ratio => strategy.profit_ratio(), + OptimizationCriteria::Area => strategy.profit_area(), + }; + + if current_value > best_value { + // Update the best value and replace the current strategy + info!("Found better value: {}", current_value); + best_value = current_value; + *self = strategy.clone(); } } } - fn are_valid_prices(&self, legs: &StrategyLegs) -> bool { - let (long, short) = match legs { - StrategyLegs::TwoLegs { first, second } => (first, second), - _ => panic!("Invalid number of legs for this strategy"), - }; - long.put_ask.unwrap_or(PZERO) > PZERO && short.put_bid.unwrap_or(PZERO) > PZERO - } - fn create_strategy(&self, chain: &OptionChain, legs: &StrategyLegs) -> Self::Strategy { - let (long, short) = match legs { + let (short, long) = match legs { StrategyLegs::TwoLegs { first, second } => (first, second), _ => panic!("Invalid number of legs for this strategy"), }; @@ -897,11 +891,12 @@ mod tests_bear_put_spread_validation { #[cfg(test)] mod tests_bear_put_spread_optimization { use super::*; + use crate::model::types::ExpirationDate; use crate::spos; fn create_test_chain() -> OptionChain { - let mut chain = OptionChain::new("TEST", pos!(90.0), "2024-12-31".to_string()); + let mut chain = OptionChain::new("TEST", pos!(90.0), "2024-12-31".to_string(), None, None); // Add options with increasing strikes around the current price chain.add_option( @@ -1069,67 +1064,6 @@ mod tests_bear_put_spread_optimization { assert!(spread.long_put.option.strike_price <= pos!(105.0)); } - #[test] - fn test_are_valid_prices() { - let spread = create_base_spread(); - - // Test with valid prices - let valid_option1 = OptionData::new( - pos!(105.0), - None, - None, - spos!(1.5), - spos!(1.7), - spos!(0.2), - Some(-0.4), - spos!(100.0), - Some(50), - ); - - let valid_option2 = OptionData::new( - pos!(95.0), - None, - None, - spos!(4.0), - spos!(4.2), - spos!(0.2), - Some(-0.6), - spos!(100.0), - Some(50), - ); - - let legs = StrategyLegs::TwoLegs { - first: &valid_option1, - second: &valid_option2, - }; - assert!(spread.are_valid_prices(&legs)); - - // Test with invalid prices (zero or None) - let invalid_option = OptionData::new( - pos!(100.0), - None, - None, - None, - None, - spos!(0.2), - Some(-0.5), - spos!(100.0), - Some(50), - ); - - let legs = StrategyLegs::TwoLegs { - first: &invalid_option, - second: &valid_option2, - }; - assert!(!spread.are_valid_prices(&legs)); - - let legs = StrategyLegs::TwoLegs { - first: &valid_option1, - second: &invalid_option, - }; - assert!(!spread.are_valid_prices(&legs)); - } - #[test] fn test_create_strategy() { let spread = create_base_spread(); @@ -1138,12 +1072,12 @@ mod tests_bear_put_spread_optimization { let long_option = chain .options .iter() - .find(|o| o.strike_price == pos!(105.0)) + .find(|o| o.strike_price == pos!(95.0)) .unwrap(); let short_option = chain .options .iter() - .find(|o| o.strike_price == pos!(95.0)) + .find(|o| o.strike_price == pos!(105.0)) .unwrap(); let legs = StrategyLegs::TwoLegs { @@ -1217,6 +1151,246 @@ mod tests_bear_put_spread_optimization { } } +#[cfg(test)] +mod tests_bear_put_spread_optimizable { + use super::*; + use crate::model::types::{ExpirationDate, PositiveF64}; + use crate::spos; + use crate::strategies::utils::FindOptimalSide; + use crate::utils::setup_logger; + + fn create_mock_option_chain() -> OptionChain { + let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-03-15".to_string(), None, None); + + // Para un Bear Put Spread, necesitamos: + // - Strikes por encima y por debajo del precio actual + // - Puts con precios realistas basados en la distancia al strike + + // Strike por debajo del spot (95) + chain.add_option( + pos!(95.0), // strike + spos!(0.5), // call_bid + spos!(0.7), // call_ask + spos!(2.0), // put_bid - menor precio por estar OTM + spos!(2.2), // put_ask + spos!(0.2), // implied_vol + Some(-0.3), // delta + spos!(100.0), // volume + Some(50), // open_interest + ); + + // Strike ATM (100) + chain.add_option( + pos!(100.0), + spos!(2.8), + spos!(3.0), + spos!(4.8), + spos!(5.0), + spos!(0.2), + Some(-0.5), + spos!(200.0), + Some(100), + ); + + // Strike por encima del spot (105) + chain.add_option( + pos!(105.0), + spos!(5.8), + spos!(6.0), + spos!(8.8), // put_bid - mayor precio por estar ITM + spos!(9.0), // put_ask + spos!(0.2), + Some(-0.7), + spos!(150.0), + Some(75), + ); + + chain + } + + fn create_test_bear_put_spread() -> BearPutSpread { + BearPutSpread::new( + "TEST".to_string(), + pos!(100.0), // underlying_price + pos!(105.0), // long strike (higher) + pos!(95.0), // short strike (lower) + ExpirationDate::Days(30.0), + 0.2, + 0.05, + 0.0, + pos!(1.0), + 2.0, // premium short put + 8.8, // premium long put + 0.0, + 0.0, + 0.0, + 0.0, + ) + } + + #[test] + fn test_filter_valid_combinations() { + setup_logger(); + let spread = create_test_bear_put_spread(); + let chain = create_mock_option_chain(); + + // Debug output para ayudar en el diagnóstico + info!("Chain options:"); + for option in chain.options.iter() { + info!( + "Strike: {}, Put bid: {:?}, Put ask: {:?}", + option.strike_price, option.put_bid, option.put_ask + ); + } + + let combinations: Vec<_> = spread + .filter_combinations(&chain, FindOptimalSide::All) + .collect(); + + info!("Found {} combinations", combinations.len()); + + assert!( + !combinations.is_empty(), + "Should find at least one valid combination" + ); + + for combination in combinations { + match combination { + OptionDataGroup::Two(short, long) => { + // Short strike should be lower than long strike + assert!(short.strike_price < long.strike_price); + + // Both options should have valid put prices + assert!( + short.put_bid.is_some(), + "Short put bid is missing for strike {}", + short.strike_price + ); + assert!( + long.put_ask.is_some(), + "Long put ask is missing for strike {}", + long.strike_price + ); + + // Both options should have valid implied volatility + assert!(short.implied_volatility.is_some()); + assert!(long.implied_volatility.is_some()); + + info!( + "Valid combination - Short strike: {}, Long strike: {}", + short.strike_price, long.strike_price + ); + } + _ => panic!("Expected Two-leg combination"), + } + } + } + + #[test] + fn test_filter_invalid_prices() { + let mut chain = create_mock_option_chain(); + // Add an option with invalid put prices + chain.add_option( + pos!(97.0), + spos!(1.0), + spos!(1.2), + None, // Invalid put_bid + None, // Invalid put_ask + spos!(0.2), + Some(-0.4), + spos!(50.0), + Some(25), + ); + + let spread = create_test_bear_put_spread(); + let combinations: Vec<_> = spread + .filter_combinations(&chain, FindOptimalSide::Lower) + .collect(); + + for combination in combinations { + match combination { + OptionDataGroup::Two(short, long) => { + // Verify that options with invalid prices are filtered out + assert!(short.put_bid.unwrap() > PZERO); + assert!(long.put_ask.unwrap() > PZERO); + } + _ => panic!("Expected Two-leg combination"), + } + } + } + + #[test] + fn test_filter_with_different_optimal_sides() { + let spread = create_test_bear_put_spread(); + let chain = create_mock_option_chain(); + + // Test Lower side (typical for bear put spread) + let lower_combinations: Vec<_> = spread + .filter_combinations(&chain, FindOptimalSide::Lower) + .collect(); + assert!(!lower_combinations.is_empty()); + + // Test Upper side (should have fewer or no valid combinations) + let upper_combinations: Vec<_> = spread + .filter_combinations(&chain, FindOptimalSide::Upper) + .collect(); + + // Test All sides + let all_combinations: Vec<_> = spread + .filter_combinations(&chain, FindOptimalSide::All) + .collect(); + + assert!(all_combinations.len() >= lower_combinations.len()); + assert!(all_combinations.len() >= upper_combinations.len()); + } + + #[test] + fn test_filter_empty_chain() { + let spread = create_test_bear_put_spread(); + let empty_chain = + OptionChain::new("TEST", pos!(100.0), "2024-03-15".to_string(), None, None); + + let combinations: Vec<_> = spread + .filter_combinations(&empty_chain, FindOptimalSide::Lower) + .collect(); + + assert!(combinations.is_empty()); + } + + #[test] + fn test_filter_strategy_constraints() { + let spread = create_test_bear_put_spread(); + let mut chain = create_mock_option_chain(); + + // Add an option that would create an invalid strategy (strikes too close) + chain.add_option( + pos!(99.9), + spos!(1.0), + spos!(1.2), + spos!(3.0), + spos!(3.2), + spos!(0.2), + Some(-0.5), + spos!(50.0), + Some(25), + ); + + let combinations: Vec<_> = spread + .filter_combinations(&chain, FindOptimalSide::Lower) + .collect(); + + for combination in combinations { + match combination { + OptionDataGroup::Two(short, long) => { + // Verify that the strikes have enough width between them + assert!((long.strike_price - short.strike_price).value() >= 1.0); + } + _ => panic!("Expected Two-leg combination"), + } + } + } +} + #[cfg(test)] mod tests_bear_put_spread_profit { use super::*; diff --git a/src/strategies/bull_call_spread.rs b/src/strategies/bull_call_spread.rs index 96c4e103..f5c98c42 100644 --- a/src/strategies/bull_call_spread.rs +++ b/src/strategies/bull_call_spread.rs @@ -16,7 +16,8 @@ Key characteristics: */ use super::base::{Optimizable, Positionable, Strategies, StrategyType, Validable}; -use crate::chains::chain::{OptionChain, OptionData}; +use crate::chains::chain::OptionChain; +use crate::chains::utils::OptionDataGroup; use crate::chains::StrategyLegs; use crate::constants::{DARK_BLUE, DARK_GREEN, ZERO}; use crate::greeks::equations::{Greek, Greeks}; @@ -38,7 +39,7 @@ use crate::visualization::utils::Graph; use chrono::Utc; use plotters::prelude::full_palette::ORANGE; use plotters::prelude::{ShapeStyle, RED}; -use tracing::{debug, error}; +use tracing::{debug, error, info}; const BULL_CALL_SPREAD_DESCRIPTION: &str = "A bull call spread is created by buying a call option with a lower strike price \ @@ -242,7 +243,10 @@ impl Validable for BullCallSpread { return false; } if self.long_call.option.strike_price >= self.short_call.option.strike_price { - error!("Long call strike price must be lower than short call strike price"); + error!( + "Long call strike price {} must be lower than short call strike price {}", + self.long_call.option.strike_price, self.short_call.option.strike_price + ); return false; } @@ -253,72 +257,70 @@ impl Validable for BullCallSpread { impl Optimizable for BullCallSpread { type Strategy = BullCallSpread; + fn filter_combinations<'a>( + &'a self, + option_chain: &'a OptionChain, + side: FindOptimalSide, + ) -> impl Iterator> { + let underlying_price = self.get_underlying_price(); + let strategy = self.clone(); + option_chain + .get_double_iter() + // Filter out invalid combinations based on FindOptimalSide + .filter(move |&option| { + option.0.is_valid_optimal_side(underlying_price, &side) + && option.1.is_valid_optimal_side(underlying_price, &side) + }) + // Filter out options with invalid bid/ask prices + .filter(|(long, short)| { + long.call_ask.unwrap_or(PZERO) > PZERO && short.call_bid.unwrap_or(PZERO) > PZERO + }) + // Filter out options that don't meet strategy constraints + .filter(move |(long, short)| { + let legs = StrategyLegs::TwoLegs { + first: long, + second: short, + }; + let strategy = strategy.create_strategy(option_chain, &legs); + strategy.validate() && strategy.max_profit().is_ok() && strategy.max_loss().is_ok() + }) + // Map to OptionDataGroup + .map(move |(long, short)| OptionDataGroup::Two(long, short)) + } + fn find_optimal( &mut self, option_chain: &OptionChain, side: FindOptimalSide, criteria: OptimizationCriteria, ) { - let options: Vec<&OptionData> = option_chain.options.iter().collect(); let mut best_value = f64::NEG_INFINITY; - - for long_index in 0..options.len() { - let long_option = &options[long_index]; - - for short_option in &options[long_index + 1..] { - if !self.is_valid_long_option(long_option, &side) - || !self.is_valid_short_option(short_option, &side) - { - debug!( - "Invalid options Asset {} - Long({}) Short({})", - option_chain.underlying_price, - long_option.strike_price, - short_option.strike_price, - ); - continue; - } - - let legs = StrategyLegs::TwoLegs { - first: long_option, - second: short_option, - }; - - if !self.are_valid_prices(&legs) { - debug!( - "Invalid prices - Long({}): {:?} Short({}): {:?}", - long_option.strike_price, - long_option.call_ask, - short_option.strike_price, - short_option.call_bid - ); - continue; - } - - let strategy = self.create_strategy(option_chain, &legs); - - if !strategy.validate() { - debug!("Invalid strategy"); - continue; - } - - if strategy.max_profit().is_err() || strategy.max_loss().is_err() { - debug!( - "Invalid profit {} loss {}", - strategy.max_profit().unwrap_or(PZERO), - strategy.max_loss().unwrap_or(PZERO) - ); - continue; - } - - let current_value = match criteria { - OptimizationCriteria::Ratio => strategy.profit_ratio(), - OptimizationCriteria::Area => strategy.profit_area(), - }; - - if current_value > best_value { - best_value = current_value; - *self = strategy.clone(); - } + let strategy_clone = self.clone(); + let options_iter = strategy_clone.filter_combinations(option_chain, side); + + for option_data_group in options_iter { + // Unpack the OptionDataGroup into individual options + let (long, short) = match option_data_group { + OptionDataGroup::Two(first, second) => (first, second), + _ => panic!("Invalid OptionDataGroup"), + }; + + let legs = StrategyLegs::TwoLegs { + first: long, + second: short, + }; + let strategy = self.create_strategy(option_chain, &legs); + // Calculate the current value based on the optimization criteria + let current_value = match criteria { + OptimizationCriteria::Ratio => strategy.profit_ratio(), + OptimizationCriteria::Area => strategy.profit_area(), + }; + + if current_value > best_value { + // Update the best value and replace the current strategy + info!("Found better value: {}", current_value); + best_value = current_value; + *self = strategy.clone(); } } } @@ -911,11 +913,12 @@ mod tests_bull_call_spread_validation { #[cfg(test)] mod tests_bull_call_spread_optimization { use super::*; + use crate::chains::chain::OptionData; use crate::model::types::ExpirationDate; use crate::spos; fn create_test_chain() -> OptionChain { - let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-12-31".to_string()); + let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-12-31".to_string(), None, None); chain.add_option( pos!(85.0), // strike diff --git a/src/strategies/bull_put_spread.rs b/src/strategies/bull_put_spread.rs index 9a210335..9358ff56 100644 --- a/src/strategies/bull_put_spread.rs +++ b/src/strategies/bull_put_spread.rs @@ -15,7 +15,8 @@ Key characteristics: - Also known as a vertical put credit spread */ use super::base::{Optimizable, Positionable, Strategies, StrategyType, Validable}; -use crate::chains::chain::{OptionChain, OptionData}; +use crate::chains::chain::OptionChain; +use crate::chains::utils::OptionDataGroup; use crate::chains::StrategyLegs; use crate::constants::{DARK_BLUE, DARK_GREEN, ZERO}; use crate::greeks::equations::{Greek, Greeks}; @@ -150,6 +151,110 @@ impl BullPutSpread { strategy } +} + +impl Positionable for BullPutSpread { + fn add_position(&mut self, position: &Position) -> Result<(), String> { + match position.option.side { + Side::Short => { + self.short_put = position.clone(); + Ok(()) + } + Side::Long => { + self.long_put = position.clone(); + Ok(()) + } + } + } + + fn get_positions(&self) -> Result, String> { + Ok(vec![&self.long_put, &self.short_put]) + } +} + +impl Strategies for BullPutSpread { + fn get_underlying_price(&self) -> PositiveF64 { + self.short_put.option.underlying_price + } + + fn max_profit(&self) -> Result { + let net_premium_received = self.net_premium_received(); + if net_premium_received < ZERO { + trace!("Net premium received is negative {}", net_premium_received); + Err("Net premium received is negative") + } else { + Ok(pos!(net_premium_received)) + } + } + + fn max_loss(&self) -> Result { + let width = self.short_put.option.strike_price - self.long_put.option.strike_price; + let max_loss = + (width * self.short_put.option.quantity).value() - self.net_premium_received(); + if max_loss < ZERO { + trace!("Max loss is negative {}", max_loss); + Err("Max loss is negative") + } else { + Ok(pos!(max_loss)) + } + } + + fn total_cost(&self) -> PositiveF64 { + pos!(self.long_put.net_cost() + self.short_put.net_cost()) + } + + fn net_premium_received(&self) -> f64 { + self.short_put.net_premium_received() - self.long_put.net_cost() + } + + fn fees(&self) -> f64 { + self.long_put.open_fee + + self.long_put.close_fee + + self.short_put.open_fee + + self.short_put.close_fee + } + + fn profit_area(&self) -> f64 { + let high = self.max_profit().unwrap_or(PZERO); + let base = self.short_put.option.strike_price - self.break_even_points[0]; + (high * base / 200.0).value() + } + + fn profit_ratio(&self) -> f64 { + let max_profit = self.max_profit().unwrap_or(PZERO); + let max_loss = self.max_loss().unwrap_or(PZERO); + match (max_profit, max_loss) { + (PZERO, _) => ZERO, + (_, PZERO) => f64::INFINITY, + _ => (max_profit / max_loss * 100.0).value(), + } + } + + fn get_break_even_points(&self) -> Vec { + self.break_even_points.clone() + } +} + +impl Validable for BullPutSpread { + fn validate(&self) -> bool { + if !self.long_put.validate() { + debug!("Long put is invalid"); + return false; + } + if !self.short_put.validate() { + debug!("Short put is invalid"); + return false; + } + if self.long_put.option.strike_price >= self.short_put.option.strike_price { + debug!("Long put strike price must be lower than short put strike price"); + return false; + } + true + } +} + +impl Optimizable for BullPutSpread { + type Strategy = BullPutSpread; /// Filters combinations of `OptionData` from the provided `OptionChain` /// based on validity, pricing conditions, and strategy constraints. @@ -193,15 +298,18 @@ impl BullPutSpread { /// # Examples /// /// ```rust + /// use tracing::info; /// use optionstratlib::chains::chain::OptionChain; + /// use optionstratlib::chains::utils::OptionDataGroup; /// use optionstratlib::model::types::ExpirationDate; /// use optionstratlib::model::types::PositiveF64; /// use optionstratlib::pos; + /// use optionstratlib::strategies::base::Optimizable; /// use optionstratlib::strategies::bull_put_spread::BullPutSpread; /// use optionstratlib::strategies::utils::FindOptimalSide; /// /// let underlying_price = pos!(5810.0); - /// let option_chain = OptionChain::new("TEST", underlying_price, "2024-01-01".to_string()); + /// let option_chain = OptionChain::new("TEST", underlying_price, "2024-01-01".to_string(), None, None); /// let bull_put_spread_strategy = BullPutSpread::new( /// "SP500".to_string(), /// underlying_price, // underlying_price @@ -223,8 +331,12 @@ impl BullPutSpread { /// let side = FindOptimalSide::Lower; /// let filtered_combinations = bull_put_spread_strategy.filter_combinations(&option_chain, side); /// - /// for (long, short) in filtered_combinations { - /// println!("Long Option: {:?}, Short Option: {:?}", long, short); + /// for option_data_group in filtered_combinations { + /// let (long, short) = match option_data_group { + /// OptionDataGroup::Two(first, second) => (first, second), + /// _ => panic!("Invalid OptionDataGroup"), + /// }; + /// info!("Long Option: {:?}, Short Option: {:?}", long, short); /// } /// ``` /// @@ -240,22 +352,25 @@ impl BullPutSpread { /// - [`OptionChain::get_double_iter`](crate::chains::chain::OptionChain::get_double_iter) /// - [`OptionData::is_valid_optimal_side`](crate::chains::chain::OptionData::is_valid_optimal_side) /// - [`BullPutSpread::validate`](crate::strategies::bull_put_spread::BullPutSpread::validate) - pub fn filter_combinations<'a>( + fn filter_combinations<'a>( &'a self, option_chain: &'a OptionChain, side: FindOptimalSide, - ) -> impl Iterator { + ) -> impl Iterator> { let underlying_price = self.get_underlying_price(); let strategy = self.clone(); option_chain .get_double_iter() + // Filter out invalid combinations based on FindOptimalSide .filter(move |&option| { option.0.is_valid_optimal_side(underlying_price, &side) && option.1.is_valid_optimal_side(underlying_price, &side) }) + // Filter out options with invalid bid/ask prices .filter(|(long, short)| { long.put_ask.unwrap_or(PZERO) > PZERO && short.put_bid.unwrap_or(PZERO) > PZERO }) + // Filter out options that don't meet strategy constraints .filter(move |(long_option, short_option)| { let legs = StrategyLegs::TwoLegs { first: long_option, @@ -264,112 +379,10 @@ impl BullPutSpread { let strategy = strategy.create_strategy(option_chain, &legs); strategy.validate() && strategy.max_profit().is_ok() && strategy.max_loss().is_ok() }) - } -} - -impl Positionable for BullPutSpread { - fn add_position(&mut self, position: &Position) -> Result<(), String> { - match position.option.side { - Side::Short => { - self.short_put = position.clone(); - Ok(()) - } - Side::Long => { - self.long_put = position.clone(); - Ok(()) - } - } + // Map to OptionDataGroup + .map(move |(long, short)| OptionDataGroup::Two(long, short)) } - fn get_positions(&self) -> Result, String> { - Ok(vec![&self.long_put, &self.short_put]) - } -} - -impl Strategies for BullPutSpread { - fn get_underlying_price(&self) -> PositiveF64 { - self.short_put.option.underlying_price - } - - fn max_profit(&self) -> Result { - let net_premium_received = self.net_premium_received(); - if net_premium_received < ZERO { - trace!("Net premium received is negative {}", net_premium_received); - Err("Net premium received is negative") - } else { - Ok(pos!(net_premium_received)) - } - } - - fn max_loss(&self) -> Result { - let width = self.short_put.option.strike_price - self.long_put.option.strike_price; - let max_loss = - (width * self.short_put.option.quantity).value() - self.net_premium_received(); - if max_loss < ZERO { - trace!("Max loss is negative {}", max_loss); - Err("Max loss is negative") - } else { - Ok(pos!(max_loss)) - } - } - - fn total_cost(&self) -> PositiveF64 { - pos!(self.long_put.net_cost() + self.short_put.net_cost()) - } - - fn net_premium_received(&self) -> f64 { - self.short_put.net_premium_received() - self.long_put.net_cost() - } - - fn fees(&self) -> f64 { - self.long_put.open_fee - + self.long_put.close_fee - + self.short_put.open_fee - + self.short_put.close_fee - } - - fn profit_area(&self) -> f64 { - let high = self.max_profit().unwrap_or(PZERO); - let base = self.short_put.option.strike_price - self.break_even_points[0]; - (high * base / 200.0).value() - } - - fn profit_ratio(&self) -> f64 { - let max_profit = self.max_profit().unwrap_or(PZERO); - let max_loss = self.max_loss().unwrap_or(PZERO); - match (max_profit, max_loss) { - (PZERO, _) => ZERO, - (_, PZERO) => f64::INFINITY, - _ => (max_profit / max_loss * 100.0).value(), - } - } - - fn get_break_even_points(&self) -> Vec { - self.break_even_points.clone() - } -} - -impl Validable for BullPutSpread { - fn validate(&self) -> bool { - if !self.long_put.validate() { - debug!("Long put is invalid"); - return false; - } - if !self.short_put.validate() { - debug!("Short put is invalid"); - return false; - } - if self.long_put.option.strike_price >= self.short_put.option.strike_price { - debug!("Long put strike price must be lower than short put strike price"); - return false; - } - true - } -} - -impl Optimizable for BullPutSpread { - type Strategy = BullPutSpread; - fn find_optimal( &mut self, option_chain: &OptionChain, @@ -380,18 +393,26 @@ impl Optimizable for BullPutSpread { let strategy_clone = self.clone(); let options_iter = strategy_clone.filter_combinations(option_chain, side); - for (long_option, short_option) in options_iter { + for option_data_group in options_iter { + // Unpack the OptionDataGroup into individual options + let (long_option, short_option) = match option_data_group { + OptionDataGroup::Two(first, second) => (first, second), + _ => panic!("Invalid OptionDataGroup"), + }; + let legs = StrategyLegs::TwoLegs { first: long_option, second: short_option, }; let strategy = self.create_strategy(option_chain, &legs); + // Calculate the current value based on the optimization criteria let current_value = match criteria { OptimizationCriteria::Ratio => strategy.profit_ratio(), OptimizationCriteria::Area => strategy.profit_area(), }; if current_value > best_value { + // Update the best value and replace the current strategy debug!("Found better value: {}", current_value); best_value = current_value; *self = strategy.clone(); @@ -969,11 +990,12 @@ mod tests_bull_put_spread_validation { #[cfg(test)] mod tests_bull_put_spread_optimization { use super::*; + use crate::chains::chain::OptionData; use crate::model::types::ExpirationDate; use crate::spos; fn create_test_chain() -> OptionChain { - let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-12-31".to_string()); + let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-12-31".to_string(), None, None); chain.add_option( pos!(85.0), // strike diff --git a/src/strategies/butterfly_spread.rs b/src/strategies/butterfly_spread.rs index 50afb014..fac48f6f 100644 --- a/src/strategies/butterfly_spread.rs +++ b/src/strategies/butterfly_spread.rs @@ -17,7 +17,8 @@ Key characteristics: */ use super::base::{Optimizable, Positionable, Strategies, StrategyType, Validable}; -use crate::chains::chain::{OptionChain, OptionData}; +use crate::chains::chain::OptionChain; +use crate::chains::utils::OptionDataGroup; use crate::chains::StrategyLegs; use crate::constants::{DARK_BLUE, DARK_GREEN, ZERO}; use crate::greeks::equations::{Greek, Greeks}; @@ -327,6 +328,47 @@ impl Strategies for LongButterflySpread { impl Optimizable for LongButterflySpread { type Strategy = LongButterflySpread; + fn filter_combinations<'a>( + &'a self, + option_chain: &'a OptionChain, + side: FindOptimalSide, + ) -> impl Iterator> { + let underlying_price = self.get_underlying_price(); + let strategy = self.clone(); + option_chain + .get_triple_iter() + // Filter out invalid combinations based on FindOptimalSide + .filter(move |(long_low, short, long_high)| { + long_low.is_valid_optimal_side(underlying_price, &side) + && short.is_valid_optimal_side(underlying_price, &side) + && long_high.is_valid_optimal_side(underlying_price, &side) + }) + .filter(move |(long_low, short, long_high)| { + long_low.strike_price < short.strike_price + && short.strike_price < long_high.strike_price + }) + // Filter out options with invalid bid/ask prices + .filter(|(long_low, short, long_high)| { + long_low.call_ask.unwrap_or(PZERO) > PZERO + && short.call_bid.unwrap_or(PZERO) > PZERO + && long_high.call_ask.unwrap_or(PZERO) > PZERO + }) + // Filter out options that don't meet strategy constraints + .filter(move |(long_low, short, long_high)| { + let legs = StrategyLegs::ThreeLegs { + first: long_low, + second: short, + third: long_high, + }; + let strategy = strategy.create_strategy(option_chain, &legs); + strategy.validate() && strategy.max_profit().is_ok() && strategy.max_loss().is_ok() + }) + // Map to OptionDataGroup + .map(move |(long_low, short, long_high)| { + OptionDataGroup::Three(long_low, short, long_high) + }) + } + fn find_optimal( &mut self, option_chain: &OptionChain, @@ -334,108 +376,34 @@ impl Optimizable for LongButterflySpread { criteria: OptimizationCriteria, ) { let mut best_value = f64::NEG_INFINITY; - let options: Vec<&OptionData> = option_chain.options.iter().collect(); - - for (i, long_call_low) in options.iter().enumerate() { - if !self.is_valid_long_option(long_call_low, &side) { - continue; - } - - for (j, short_calls) in options.iter().enumerate().skip(i + 1) { - if !self.is_valid_short_option(short_calls, &side) { - continue; - } - - for long_call_high in options.iter().skip(j + 1) { - if !self.is_valid_long_option(long_call_high, &side) { - continue; - } - - if long_call_low.strike_price >= short_calls.strike_price - || short_calls.strike_price >= long_call_high.strike_price - { - error!("Invalid order of strikes"); - continue; - } - - if !self.are_valid_prices(&StrategyLegs::ThreeLegs { - first: long_call_low, - second: short_calls, - third: long_call_high, - }) { - error!("Invalid prices"); - continue; - } - - let new_strategy = self.create_strategy( - option_chain, - &StrategyLegs::ThreeLegs { - first: long_call_low, - second: short_calls, - third: long_call_high, - }, - ); - - if !new_strategy.validate() { - debug!("Invalid strategy"); - continue; - } - - if new_strategy.max_profit().is_err() || new_strategy.max_loss().is_err() { - continue; - } - - let current_value = match criteria { - OptimizationCriteria::Ratio => new_strategy.profit_ratio(), - OptimizationCriteria::Area => new_strategy.profit_area(), - }; - - if current_value > best_value { - info!("New best value: {}", current_value); - best_value = current_value; - *self = new_strategy; - } - } - } - } - } - - fn is_valid_short_option(&self, option: &OptionData, side: &FindOptimalSide) -> bool { - let is_valid_strike = match side { - FindOptimalSide::Upper => option.strike_price >= self.get_underlying_price(), - FindOptimalSide::Lower => option.strike_price <= self.get_underlying_price(), - FindOptimalSide::All => true, - FindOptimalSide::Range(start, end) => { - option.strike_price >= *start && option.strike_price <= *end - } - }; - is_valid_strike && option.call_bid.unwrap_or(PZERO) > PZERO - } - - fn is_valid_long_option(&self, option: &OptionData, side: &FindOptimalSide) -> bool { - let is_valid_strike = match side { - FindOptimalSide::Upper => option.strike_price >= self.get_underlying_price(), - FindOptimalSide::Lower => option.strike_price <= self.get_underlying_price(), - FindOptimalSide::All => true, - FindOptimalSide::Range(start, end) => { - option.strike_price >= *start && option.strike_price <= *end - } - }; - is_valid_strike && option.call_ask.unwrap_or(PZERO) > PZERO - } - - fn are_valid_prices(&self, legs: &StrategyLegs) -> bool { - match legs { - StrategyLegs::ThreeLegs { - first: low_strike, - second: middle_strike, - third: high_strike, - } => { - low_strike.call_ask.unwrap_or(PZERO) > PZERO - && middle_strike.call_bid.unwrap_or(PZERO) > PZERO - && high_strike.call_ask.unwrap_or(PZERO) > PZERO + let strategy_clone = self.clone(); + let options_iter = strategy_clone.filter_combinations(option_chain, side); + + for option_data_group in options_iter { + // Unpack the OptionDataGroup into individual options + let (long_low, short, long_high) = match option_data_group { + OptionDataGroup::Three(first, second, third) => (first, second, third), + _ => panic!("Invalid OptionDataGroup"), + }; + + let legs = StrategyLegs::ThreeLegs { + first: long_low, + second: short, + third: long_high, + }; + let strategy = self.create_strategy(option_chain, &legs); + // Calculate the current value based on the optimization criteria + let current_value = match criteria { + OptimizationCriteria::Ratio => strategy.profit_ratio(), + OptimizationCriteria::Area => strategy.profit_area(), + }; + + if current_value > best_value { + // Update the best value and replace the current strategy + info!("Found better value: {}", current_value); + best_value = current_value; + *self = strategy.clone(); } - _ => false, } } @@ -497,7 +465,10 @@ impl Graph for LongButterflySpread { "Long Call High Strike: ${}", self.long_call_high.option.strike_price ), - format!("Expire: {}", self.long_call_low.option.expiration_date), + format!( + "Expire: {}", + self.long_call_low.option.expiration_date.get_date_string() + ), ]; format!("{}\n\t{}", strategy_title, leg_titles.join("\n\t")) @@ -526,6 +497,7 @@ impl Graph for LongButterflySpread { let left_loss = self.calculate_profit_at(self.long_call_low.option.strike_price); let right_loss = self.calculate_profit_at(self.long_call_high.option.strike_price); + let font_size = 24; // Break-even points points.extend( self.break_even_points @@ -542,7 +514,7 @@ impl Graph for LongButterflySpread { point_color: DARK_BLUE, label_color: DARK_BLUE, point_size: 5, - font_size: 18, + font_size, }), ); @@ -557,7 +529,7 @@ impl Graph for LongButterflySpread { point_color: DARK_GREEN, label_color: DARK_GREEN, point_size: 5, - font_size: 18, + font_size, }); let left_color = if left_loss > ZERO { DARK_GREEN } else { RED }; @@ -570,19 +542,19 @@ impl Graph for LongButterflySpread { point_color: left_color, label_color: left_color, point_size: 5, - font_size: 18, + font_size, }); let right_color = if right_loss > ZERO { DARK_GREEN } else { RED }; points.push(ChartPoint { coordinates: (self.long_call_high.option.strike_price.value(), right_loss), - label: format!("Max Loss {:.2}", right_loss), + label: format!("Right Loss {:.2}", right_loss), label_offset: LabelOffsetType::Relative(3.0, -3.0), point_color: right_color, label_color: right_color, point_size: 5, - font_size: 18, + font_size, }); // Current price point @@ -1045,6 +1017,47 @@ impl Strategies for ShortButterflySpread { impl Optimizable for ShortButterflySpread { type Strategy = ShortButterflySpread; + fn filter_combinations<'a>( + &'a self, + option_chain: &'a OptionChain, + side: FindOptimalSide, + ) -> impl Iterator> { + let underlying_price = self.get_underlying_price(); + let strategy = self.clone(); + option_chain + .get_triple_iter() + // Filter out invalid combinations based on FindOptimalSide + .filter(move |(short_low, long, short_high)| { + short_low.is_valid_optimal_side(underlying_price, &side) + && long.is_valid_optimal_side(underlying_price, &side) + && short_high.is_valid_optimal_side(underlying_price, &side) + }) + .filter(move |(short_low, long, short_high)| { + short_low.strike_price < long.strike_price + && long.strike_price < short_high.strike_price + }) + // Filter out options with invalid bid/ask prices + .filter(|(short_low, long, short_high)| { + short_low.call_bid.unwrap_or(PZERO) > PZERO + && long.call_ask.unwrap_or(PZERO) > PZERO + && short_high.call_bid.unwrap_or(PZERO) > PZERO + }) + // Filter out options that don't meet strategy constraints + .filter(move |(short_low, long, short_high)| { + let legs = StrategyLegs::ThreeLegs { + first: short_low, + second: long, + third: short_high, + }; + let strategy = strategy.create_strategy(option_chain, &legs); + strategy.validate() && strategy.max_profit().is_ok() && strategy.max_loss().is_ok() + }) + // Map to OptionDataGroup + .map(move |(short_low, long, short_high)| { + OptionDataGroup::Three(short_low, long, short_high) + }) + } + fn find_optimal( &mut self, option_chain: &OptionChain, @@ -1052,108 +1065,34 @@ impl Optimizable for ShortButterflySpread { criteria: OptimizationCriteria, ) { let mut best_value = f64::NEG_INFINITY; - let options: Vec<&OptionData> = option_chain.options.iter().collect(); - - for (i, short_call_low) in options.iter().enumerate() { - if !self.is_valid_short_option(short_call_low, &side) { - continue; - } - - for (j, long_calls) in options.iter().enumerate().skip(i + 1) { - if !self.is_valid_long_option(long_calls, &side) { - continue; - } - - for short_call_high in options.iter().skip(j + 1) { - if !self.is_valid_short_option(short_call_high, &side) { - continue; - } - - if short_call_low.strike_price >= long_calls.strike_price - || long_calls.strike_price >= short_call_high.strike_price - { - error!("Invalid order of strikes"); - continue; - } - - if !self.are_valid_prices(&StrategyLegs::ThreeLegs { - first: short_call_low, - second: long_calls, - third: short_call_high, - }) { - error!("Invalid prices"); - continue; - } - - let new_strategy = self.create_strategy( - option_chain, - &StrategyLegs::ThreeLegs { - first: short_call_low, - second: long_calls, - third: short_call_high, - }, - ); - - if !new_strategy.validate() { - debug!("Invalid strategy"); - continue; - } - - if new_strategy.max_profit().is_err() || new_strategy.max_loss().is_err() { - continue; - } - - let current_value = match criteria { - OptimizationCriteria::Ratio => new_strategy.profit_ratio(), - OptimizationCriteria::Area => new_strategy.profit_area(), - }; - - if current_value > best_value { - info!("New best value: {}", current_value); - best_value = current_value; - *self = new_strategy; - } - } - } - } - } - - fn is_valid_short_option(&self, option: &OptionData, side: &FindOptimalSide) -> bool { - let is_valid_strike = match side { - FindOptimalSide::Upper => option.strike_price >= self.get_underlying_price(), - FindOptimalSide::Lower => option.strike_price <= self.get_underlying_price(), - FindOptimalSide::All => true, - FindOptimalSide::Range(start, end) => { - option.strike_price >= *start && option.strike_price <= *end - } - }; - is_valid_strike && option.call_bid.unwrap_or(PZERO) > PZERO - } - - fn is_valid_long_option(&self, option: &OptionData, side: &FindOptimalSide) -> bool { - let is_valid_strike = match side { - FindOptimalSide::Upper => option.strike_price >= self.get_underlying_price(), - FindOptimalSide::Lower => option.strike_price <= self.get_underlying_price(), - FindOptimalSide::All => true, - FindOptimalSide::Range(start, end) => { - option.strike_price >= *start && option.strike_price <= *end - } - }; - is_valid_strike && option.call_ask.unwrap_or(PZERO) > PZERO - } - - fn are_valid_prices(&self, legs: &StrategyLegs) -> bool { - match legs { - StrategyLegs::ThreeLegs { - first: low_strike, - second: middle_strike, - third: high_strike, - } => { - low_strike.call_bid.unwrap_or(PZERO) > PZERO - && middle_strike.call_ask.unwrap_or(PZERO) > PZERO - && high_strike.call_bid.unwrap_or(PZERO) > PZERO + let strategy_clone = self.clone(); + let options_iter = strategy_clone.filter_combinations(option_chain, side); + + for option_data_group in options_iter { + // Unpack the OptionDataGroup into individual options + let (short_low, long, short_high) = match option_data_group { + OptionDataGroup::Three(first, second, third) => (first, second, third), + _ => panic!("Invalid OptionDataGroup"), + }; + + let legs = StrategyLegs::ThreeLegs { + first: short_low, + second: long, + third: short_high, + }; + let strategy = self.create_strategy(option_chain, &legs); + // Calculate the current value based on the optimization criteria + let current_value = match criteria { + OptimizationCriteria::Ratio => strategy.profit_ratio(), + OptimizationCriteria::Area => strategy.profit_area(), + }; + + if current_value > best_value { + // Update the best value and replace the current strategy + info!("Found better value: {}", current_value); + best_value = current_value; + *self = strategy.clone(); } - _ => false, } } @@ -2473,7 +2412,7 @@ mod tests_butterfly_optimizable { use crate::spos; fn create_test_option_chain() -> OptionChain { - let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-12-31".to_string()); + let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-12-31".to_string(), None, None); for strike in [85.0, 90.0, 95.0, 100.0, 105.0, 110.0, 115.0] { chain.add_option( @@ -2557,41 +2496,6 @@ mod tests_butterfly_optimizable { ); } - #[test] - fn test_is_valid_long_option() { - let butterfly = create_test_long(); - let option = OptionData::new( - pos!(95.0), - spos!(5.0), - spos!(5.2), - spos!(5.0), - spos!(5.2), - spos!(0.2), - None, - spos!(100.0), - Some(50), - ); - - assert!(butterfly.is_valid_long_option(&option, &FindOptimalSide::All)); - assert!(butterfly.is_valid_long_option(&option, &FindOptimalSide::Lower)); - assert!(!butterfly.is_valid_long_option(&option, &FindOptimalSide::Upper)); - } - - #[test] - fn test_are_valid_prices() { - let butterfly = create_test_long(); - let chain = create_test_option_chain(); - let options: Vec<&OptionData> = chain.options.iter().collect(); - - let legs = StrategyLegs::ThreeLegs { - first: options[1], // 90.0 strike - second: options[3], // 100.0 strike - third: options[5], // 110.0 strike - }; - - assert!(butterfly.are_valid_prices(&legs)); - } - #[test] fn test_find_optimal_ratio_short() { let mut butterfly = create_test_short(); @@ -2632,41 +2536,6 @@ mod tests_butterfly_optimizable { ); } - #[test] - fn test_is_valid_short_option() { - let butterfly = create_test_short(); - let option = OptionData::new( - pos!(105.0), - spos!(5.0), - spos!(5.2), - spos!(5.0), - spos!(5.2), - spos!(0.2), - None, - spos!(100.0), - Some(50), - ); - - assert!(butterfly.is_valid_short_option(&option, &FindOptimalSide::All)); - assert!(!butterfly.is_valid_short_option(&option, &FindOptimalSide::Lower)); - assert!(butterfly.is_valid_short_option(&option, &FindOptimalSide::Upper)); - } - - #[test] - fn test_are_valid_prices_short() { - let butterfly = create_test_short(); - let chain = create_test_option_chain(); - let options: Vec<&OptionData> = chain.options.iter().collect(); - - let legs = StrategyLegs::ThreeLegs { - first: options[1], // 90.0 strike - second: options[3], // 100.0 strike - third: options[5], // 110.0 strike - }; - - assert!(butterfly.are_valid_prices(&legs)); - } - #[test] fn test_find_optimal_with_range() { let mut long_butterfly = create_test_long(); @@ -2689,33 +2558,6 @@ mod tests_butterfly_optimizable { assert!(short_butterfly.long_calls.option.strike_price >= pos!(95.0)); assert!(short_butterfly.long_calls.option.strike_price <= pos!(105.0)); } - - #[test] - fn test_invalid_prices() { - let long_butterfly = create_test_long(); - let short_butterfly = create_test_short(); - - let invalid_option = OptionData::new( - pos!(100.0), - None, // missing call_bid - None, // missing call_ask - None, - None, - spos!(0.2), - None, - spos!(100.0), - Some(50), - ); - - let legs = StrategyLegs::ThreeLegs { - first: &invalid_option, - second: &invalid_option, - third: &invalid_option, - }; - - assert!(!long_butterfly.are_valid_prices(&legs)); - assert!(!short_butterfly.are_valid_prices(&legs)); - } } #[cfg(test)] diff --git a/src/strategies/call_butterfly.rs b/src/strategies/call_butterfly.rs index b5163e33..fbdf1693 100644 --- a/src/strategies/call_butterfly.rs +++ b/src/strategies/call_butterfly.rs @@ -4,14 +4,15 @@ Date: 25/9/24 ******************************************************************************/ use super::base::{Optimizable, Positionable, Strategies, StrategyType, Validable}; -use crate::chains::chain::{OptionChain, OptionData}; +use crate::chains::chain::OptionChain; use crate::constants::DARK_BLUE; use crate::constants::{DARK_GREEN, ZERO}; use crate::greeks::equations::{Greek, Greeks}; use crate::model::option::Options; use crate::model::position::Position; -use crate::model::types::{ExpirationDate, OptionStyle, OptionType, PositiveF64, Side, PZERO}; -use crate::pos; +use crate::model::types::{ + ExpirationDate, OptionStyle, OptionType, PositiveF64, Side, INFINITY, PZERO, +}; use crate::pricing::payoff::Profit; use crate::strategies::delta_neutral::{ DeltaAdjustment, DeltaInfo, DeltaNeutrality, DELTA_THRESHOLD, @@ -19,10 +20,13 @@ use crate::strategies::delta_neutral::{ use crate::strategies::utils::{FindOptimalSide, OptimizationCriteria}; use crate::visualization::model::{ChartPoint, ChartVerticalLine, LabelOffsetType}; use crate::visualization::utils::Graph; +use crate::{pos, spos}; use chrono::Utc; use plotters::prelude::{ShapeStyle, RED}; use plotters::style::full_palette::ORANGE; -use tracing::{debug, error}; +use tracing::{error, info}; +use crate::chains::StrategyLegs; +use crate::chains::utils::OptionDataGroup; const RATIO_CALL_SPREAD_DESCRIPTION: &str = "A Ratio Call Spread involves buying one call option and selling multiple call options \ @@ -35,9 +39,9 @@ pub struct CallButterfly { pub kind: StrategyType, pub description: String, pub break_even_points: Vec, - long_call_itm: Position, - long_call_otm: Position, - short_call: Position, + long_call: Position, + short_call_low: Position, + short_call_high: Position, underlying_price: PositiveF64, } @@ -46,184 +50,128 @@ impl CallButterfly { pub fn new( underlying_symbol: String, underlying_price: PositiveF64, - long_strike_itm: PositiveF64, - long_strike_otm: PositiveF64, - short_strike: PositiveF64, + long_call_strike: PositiveF64, + short_call_low_strike: PositiveF64, + short_call_high_strike: PositiveF64, expiration: ExpirationDate, implied_volatility: f64, risk_free_rate: f64, dividend_yield: f64, - long_quantity: PositiveF64, - short_quantity: PositiveF64, - premium_long_itm: f64, - premium_long_otm: f64, - premium_short: f64, + quantity: PositiveF64, + premium_long_call: f64, + premium_short_call_low: f64, + premium_short_call_high: f64, open_fee_long: f64, close_fee_long: f64, - open_fee_short: f64, - close_fee_short: f64, + open_fee_short_low: f64, + close_fee_short_low: f64, + open_fee_short_high: f64, + close_fee_short_high: f64, ) -> Self { let mut strategy = CallButterfly { name: underlying_symbol.to_string(), kind: StrategyType::CallButterfly, description: RATIO_CALL_SPREAD_DESCRIPTION.to_string(), break_even_points: Vec::new(), - long_call_itm: Position::default(), - long_call_otm: Position::default(), - short_call: Position::default(), + long_call: Position::default(), + short_call_low: Position::default(), + short_call_high: Position::default(), underlying_price, }; - let short_call_option = Options::new( + let long_call_option = Options::new( OptionType::European, - Side::Short, + Side::Long, underlying_symbol.clone(), - short_strike, + long_call_strike, expiration.clone(), implied_volatility, - short_quantity, + quantity, underlying_price, risk_free_rate, OptionStyle::Call, dividend_yield, None, ); - let short_call = Position::new( - short_call_option, - premium_short, + let long_call = Position::new( + long_call_option, + premium_long_call, Utc::now(), - open_fee_short, - close_fee_short, + open_fee_long, + close_fee_long, ); strategy - .add_position(&short_call.clone()) + .add_position(&long_call.clone()) .expect("Invalid short call"); - strategy.short_call = short_call; + strategy.long_call = long_call; - let long_call_itm_option = Options::new( + let short_call_low_option = Options::new( OptionType::European, - Side::Long, + Side::Short, underlying_symbol.clone(), - long_strike_itm, + short_call_low_strike, expiration.clone(), implied_volatility, - long_quantity, + quantity, underlying_price, risk_free_rate, OptionStyle::Call, dividend_yield, None, ); - let long_call_itm = Position::new( - long_call_itm_option, - premium_long_itm, + let short_call_low = Position::new( + short_call_low_option, + premium_short_call_low, Utc::now(), - open_fee_long, - close_fee_long, + open_fee_short_low, + close_fee_short_low, ); strategy - .add_position(&long_call_itm.clone()) + .add_position(&short_call_low.clone()) .expect("Invalid long call itm"); - strategy.long_call_itm = long_call_itm; + strategy.short_call_low = short_call_low; - let long_call_otm_option = Options::new( + let short_call_high_option = Options::new( OptionType::European, - Side::Long, + Side::Short, underlying_symbol.clone(), - long_strike_otm, + short_call_high_strike, expiration.clone(), implied_volatility, - long_quantity, + quantity, underlying_price, risk_free_rate, OptionStyle::Call, dividend_yield, None, ); - let long_call_otm = Position::new( - long_call_otm_option, - premium_long_otm, + let short_call_high = Position::new( + short_call_high_option, + premium_short_call_high, Utc::now(), - open_fee_long, - close_fee_long, + open_fee_short_high, + close_fee_short_high, ); strategy - .add_position(&long_call_otm.clone()) + .add_position(&short_call_high.clone()) .expect("Invalid long call otm"); - strategy.long_call_otm = long_call_otm; + strategy.short_call_high = short_call_high; // Calculate break-even points - let loss_at_itm_strike = - strategy.calculate_profit_at(strategy.long_call_itm.option.strike_price); - let loss_at_otm_strike = - strategy.calculate_profit_at(strategy.long_call_otm.option.strike_price); - - let first_bep = - strategy.long_call_itm.option.strike_price - (loss_at_itm_strike / long_quantity); - strategy.break_even_points.push(first_bep); + strategy.break_even_points.push( + strategy.long_call.option.strike_price + - strategy.calculate_profit_at(strategy.long_call.option.strike_price) / quantity, + ); - let second_bep = - strategy.long_call_otm.option.strike_price + (loss_at_otm_strike / long_quantity); - strategy.break_even_points.push(second_bep); + strategy.break_even_points.push( + strategy.short_call_high.option.strike_price + + strategy.calculate_profit_at(strategy.short_call_high.option.strike_price) + / quantity, + ); strategy } - fn is_valid_short_option(&self, short_option: &OptionData, side: &FindOptimalSide) -> bool { - match side { - FindOptimalSide::Upper => short_option.strike_price >= self.underlying_price, - FindOptimalSide::Lower => short_option.strike_price <= self.underlying_price, - FindOptimalSide::All => true, - FindOptimalSide::Range(start, end) => { - short_option.strike_price >= *start && short_option.strike_price <= *end - } - } - } - fn are_valid_prices( - &self, - long_itm: &OptionData, - long_otm: &OptionData, - short_option: &OptionData, - ) -> bool { - if !long_itm.valid_call() || !long_otm.valid_call() || !short_option.valid_call() { - return false; - }; - long_itm.call_ask.unwrap() > PZERO - && long_itm.call_ask.unwrap() > PZERO - && short_option.call_bid.unwrap() > PZERO - } - - fn create_strategy( - &self, - option_chain: &OptionChain, - long_itm: &OptionData, - long_otm: &OptionData, - short_option: &OptionData, - ) -> CallButterfly { - if !short_option.validate() || !long_itm.validate() || !long_otm.validate() { - panic!("Invalid options"); - } - CallButterfly::new( - option_chain.symbol.clone(), - option_chain.underlying_price, - long_itm.strike_price, - long_otm.strike_price, - short_option.strike_price, - self.short_call.option.expiration_date.clone(), - short_option.implied_volatility.unwrap().value(), - self.long_call_itm.option.risk_free_rate, - self.long_call_itm.option.dividend_yield, - self.long_call_itm.option.quantity, - self.short_call.option.quantity, - long_itm.call_ask.unwrap().value(), - long_otm.call_ask.unwrap().value(), - short_option.call_bid.unwrap().value(), - self.long_call_itm.open_fee, - self.long_call_itm.close_fee, - self.short_call.open_fee, - self.short_call.close_fee, - ) - } } impl Default for CallButterfly { @@ -235,18 +183,19 @@ impl Default for CallButterfly { PZERO, PZERO, ExpirationDate::Days(0.0), - 0.0, - 0.0, - 0.0, + ZERO, + ZERO, + ZERO, pos!(1.0), - pos!(2.0), - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, + ZERO, + ZERO, + ZERO, + ZERO, + ZERO, + ZERO, + ZERO, + ZERO, + ZERO, ) } } @@ -254,17 +203,17 @@ impl Default for CallButterfly { impl Positionable for CallButterfly { fn add_position(&mut self, position: &Position) -> Result<(), String> { match position.option.side { - Side::Long => { - if position.option.strike_price >= self.short_call.option.strike_price { - self.long_call_otm = position.clone(); + Side::Short => { + if position.option.strike_price >= self.long_call.option.strike_price { + self.short_call_high = position.clone(); Ok(()) } else { - self.long_call_itm = position.clone(); + self.short_call_low = position.clone(); Ok(()) } } - Side::Short => { - self.short_call = position.clone(); + Side::Long => { + self.long_call = position.clone(); Ok(()) } } @@ -272,9 +221,9 @@ impl Positionable for CallButterfly { fn get_positions(&self) -> Result, String> { Ok(vec![ - &self.long_call_itm, - &self.long_call_otm, - &self.short_call, + &self.long_call, + &self.short_call_low, + &self.short_call_high, ]) } } @@ -290,52 +239,62 @@ impl Strategies for CallButterfly { fn max_profit(&self) -> Result { Ok(self - .calculate_profit_at(self.short_call.option.strike_price) + .calculate_profit_at(self.long_call.option.strike_price) .abs() .into()) } fn max_loss(&self) -> Result { - let lower_loss = self.calculate_profit_at(self.long_call_itm.option.strike_price); - let upper_loss = self.calculate_profit_at(self.long_call_otm.option.strike_price); - let result = match (lower_loss > ZERO, upper_loss > ZERO) { - (true, true) => PZERO, - (true, false) => upper_loss.abs().into(), - (false, true) => lower_loss.abs().into(), - (false, false) => lower_loss.abs().max(upper_loss.abs()).into(), - }; - Ok(result) + Ok(INFINITY) } fn total_cost(&self) -> PositiveF64 { - pos!( - self.long_call_itm.net_cost() + self.long_call_otm.net_cost() - - self.short_call.net_cost() - ) + self.short_call_low.total_cost() + + self.short_call_high.total_cost() + + self.long_call.total_cost() } fn net_premium_received(&self) -> f64 { - self.short_call.net_premium_received() + let premium = self.short_call_low.net_premium_received() + + self.short_call_high.net_premium_received() + - self.long_call.net_cost(); + if premium > ZERO { + premium + } else { + ZERO + } } fn fees(&self) -> f64 { - self.long_call_itm.open_fee - + self.long_call_itm.close_fee - + self.long_call_otm.open_fee - + self.long_call_otm.close_fee - + self.short_call.open_fee * self.short_call.option.quantity - + self.short_call.close_fee * self.short_call.option.quantity + self.short_call_low.open_fee + + self.short_call_low.close_fee + + self.short_call_high.open_fee + + self.short_call_high.close_fee + + self.long_call.open_fee * self.long_call.option.quantity + + self.long_call.close_fee * self.long_call.option.quantity } fn profit_area(&self) -> f64 { - let range = self.short_call.option.strike_price - self.long_call_itm.option.strike_price; + let break_even = self.break_even(); + if break_even.len() != 2 { + panic!("Invalid break-even points"); + } + let base_low = break_even[1] - break_even[0]; let max_profit = self.max_profit().unwrap_or(PZERO); - (range.value() * max_profit / 2.0) / self.underlying_price * 100.0 + let base_high = + self.short_call_high.option.strike_price - self.short_call_low.option.strike_price; + ((base_low + base_high) * max_profit / 2.0).value() } fn profit_ratio(&self) -> f64 { - match (self.max_profit(), self.max_loss()) { - (Ok(max_profit), Ok(max_loss)) => (max_profit / max_loss * 100.0).value(), + let max_loss = match self.max_loss().unwrap_or(PZERO) { + PZERO => spos!(1.0), + INFINITY => spos!(1.0), + value => Some(value), + }; + + match (self.max_profit(), max_loss) { + (Ok(max_profit), Some(ml)) => (max_profit / ml * 100.0).value(), _ => 0.0, } } @@ -351,22 +310,25 @@ impl Validable for CallButterfly { error!("Symbol is required"); return false; } - if !self.long_call_itm.validate() { + if !self.long_call.validate() { return false; } - if !self.long_call_otm.validate() { + if !self.short_call_low.validate() { return false; } - if !self.short_call.validate() { + if !self.short_call_high.validate() { return false; } if self.underlying_price <= PZERO { error!("Underlying price must be greater than zero"); return false; } - if self.short_call.option.quantity != self.long_call_itm.option.quantity * 2.0 { - error!("Short call quantity must be twice the long call quantity and currently is short: {} and long: {}", - self.short_call.option.quantity, self.long_call_itm.option.quantity); + if self.long_call.option.strike_price >= self.short_call_low.option.strike_price { + error!("Long call strike price must be less than short call strike price"); + return false; + } + if self.short_call_low.option.strike_price >= self.short_call_high.option.strike_price { + error!("Short call low strike price must be less than short call high strike price"); return false; } true @@ -375,72 +337,127 @@ impl Validable for CallButterfly { impl Optimizable for CallButterfly { type Strategy = CallButterfly; + + fn filter_combinations<'a>( + &'a self, + option_chain: &'a OptionChain, + side: FindOptimalSide, + ) -> impl Iterator> { + let underlying_price = self.get_underlying_price(); + let strategy = self.clone(); + option_chain + .get_triple_iter() + // Filter out invalid combinations based on FindOptimalSide + .filter(move |(long, short_low, short_high)| { + long.is_valid_optimal_side(underlying_price, &side) + && short_low.is_valid_optimal_side(underlying_price, &side) + && short_high.is_valid_optimal_side(underlying_price, &side) + }) + // Filter out options with invalid bid/ask prices + .filter(|(long, short_low, short_high)| { + long.call_ask.unwrap_or(PZERO) > PZERO + && short_low.call_bid.unwrap_or(PZERO) > PZERO + && short_high.call_bid.unwrap_or(PZERO) > PZERO + }) + // Filter out options that don't meet strategy constraints + .filter(move |(long, short_low, short_high)| { + let legs = StrategyLegs::ThreeLegs { + first: long, + second: short_low, + third: short_high, + }; + let strategy = strategy.create_strategy(option_chain, &legs); + strategy.validate() && strategy.max_profit().is_ok() && strategy.max_loss().is_ok() + }) + // Map to OptionDataGroup + .map(move |(long, short_low, short_high)| OptionDataGroup::Three(long, short_low, short_high)) + } + fn find_optimal( &mut self, option_chain: &OptionChain, side: FindOptimalSide, criteria: OptimizationCriteria, ) { - let options: Vec<&OptionData> = option_chain.options.iter().collect(); let mut best_value = f64::NEG_INFINITY; - - for short_index in 1..options.len() - 1 { - let short_option = &options[short_index]; - if !self.is_valid_short_option(short_option, &side) { - debug!("Skipping short option: {}", short_option.strike_price); - continue; + let strategy_clone = self.clone(); + let options_iter = strategy_clone.filter_combinations(option_chain, side); + + for option_data_group in options_iter { + // Unpack the OptionDataGroup into individual options + let (long, short_low, short_high)= match option_data_group { + OptionDataGroup::Three(first, second, third) => (first, second, third), + _ => panic!("Invalid OptionDataGroup"), + }; + + let legs = StrategyLegs::ThreeLegs { + first: long, + second: short_low, + third: short_high, + }; + let strategy = self.create_strategy(option_chain, &legs); + // Calculate the current value based on the optimization criteria + let current_value = match criteria { + OptimizationCriteria::Ratio => strategy.profit_ratio(), + OptimizationCriteria::Area => strategy.profit_area(), + }; + + if current_value > best_value { + // Update the best value and replace the current strategy + info!("Found better value: {}", current_value); + best_value = current_value; + *self = strategy.clone(); } + } + } - for long_itm_index in 0..short_index { - let long_otm_index = short_index + (short_index - long_itm_index); - - if long_otm_index >= options.len() { - continue; - } - - let long_itm = &options[long_itm_index]; - let long_otm = &options[long_otm_index]; - - if !self.are_valid_prices(long_itm, long_otm, short_option) { - continue; - } - - let strategy = self.create_strategy(option_chain, long_itm, long_otm, short_option); - - if !strategy.validate() { - panic!("Invalid strategy"); - } - - let current_value = match criteria { - OptimizationCriteria::Ratio => strategy.profit_ratio(), - OptimizationCriteria::Area => strategy.profit_area(), - }; + fn create_strategy( + &self, + option_chain: &OptionChain, + legs: &StrategyLegs, + ) -> CallButterfly { + let (long_call, short_call_low, short_call_high) = match legs { + StrategyLegs::ThreeLegs { + first, + second, + third, + } => (first, second, third), + _ => panic!("Invalid number of legs for this strategy"), + }; - debug!( - "{}: {:.2}%", - if matches!(criteria, OptimizationCriteria::Ratio) { - "Ratio" - } else { - "Area" - }, - current_value - ); - - if current_value > best_value { - best_value = current_value; - self.clone_from(&strategy); - } - } + if !long_call.validate() || !short_call_low.validate() || !short_call_high.validate() { + panic!("Invalid options"); } + CallButterfly::new( + option_chain.symbol.clone(), + option_chain.underlying_price, + long_call.strike_price, + short_call_low.strike_price, + short_call_high.strike_price, + self.long_call.option.expiration_date.clone(), + long_call.implied_volatility.unwrap().value(), + self.long_call.option.risk_free_rate, + self.long_call.option.dividend_yield, + self.long_call.option.quantity, + long_call.call_ask.unwrap().value(), + short_call_low.call_bid.unwrap().value(), + short_call_high.call_bid.unwrap().value(), + self.long_call.open_fee, + self.long_call.close_fee, + self.short_call_low.open_fee, + self.short_call_low.close_fee, + self.short_call_high.open_fee, + self.short_call_high.close_fee, + ) } } impl Profit for CallButterfly { fn calculate_profit_at(&self, price: PositiveF64) -> f64 { let price = Some(price); - let long_call_itm_profit = self.long_call_itm.pnl_at_expiration(&price); - let long_call_otm_profit = self.long_call_otm.pnl_at_expiration(&price); - let short_call_profit = self.short_call.pnl_at_expiration(&price); + let long_call_itm_profit = self.long_call.pnl_at_expiration(&price); + let long_call_otm_profit = self.short_call_low.pnl_at_expiration(&price); + let short_call_profit = self.short_call_high.pnl_at_expiration(&price); long_call_itm_profit + long_call_otm_profit + short_call_profit } } @@ -448,9 +465,9 @@ impl Profit for CallButterfly { impl Graph for CallButterfly { fn title(&self) -> String { let strategy_title = format!("Ratio Call Spread Strategy: {:?}", self.kind); - let long_call_itm_title = self.long_call_itm.title(); - let long_call_otm_title = self.long_call_otm.title(); - let short_call_title = self.short_call.title(); + let long_call_itm_title = self.long_call.title(); + let long_call_otm_title = self.short_call_low.title(); + let short_call_title = self.short_call_high.title(); format!( "{}\n\t{}\n\t{}\n\t{}", @@ -460,11 +477,11 @@ impl Graph for CallButterfly { fn get_vertical_lines(&self) -> Vec> { let vertical_lines = vec![ChartVerticalLine { - x_coordinate: self.short_call.option.underlying_price.value(), + x_coordinate: self.long_call.option.underlying_price.value(), y_range: (f64::NEG_INFINITY, f64::INFINITY), label: format!( "Current Price: {:.2}", - self.short_call.option.underlying_price + self.long_call.option.underlying_price ), label_offset: (-24.0, -1.0), line_color: ORANGE, @@ -483,7 +500,7 @@ impl Graph for CallButterfly { points.push(ChartPoint { coordinates: (self.break_even_points[0].value(), 0.0), label: format!("Low Break Even\n\n{}", self.break_even_points[0]), - label_offset: LabelOffsetType::Relative(-26.0, 2.0), + label_offset: LabelOffsetType::Relative(-55.0, 5.0), point_color: DARK_BLUE, label_color: DARK_BLUE, point_size: 5, @@ -492,8 +509,8 @@ impl Graph for CallButterfly { points.push(ChartPoint { coordinates: (self.break_even_points[1].value(), 0.0), - label: format!("High Break Even\n\n{}", self.break_even_points[1]), - label_offset: LabelOffsetType::Relative(1.0, 2.0), + label: format!("High Break Even\n\n{}", self.break_even_points[0]), + label_offset: LabelOffsetType::Relative(3.0, 5.0), point_color: DARK_BLUE, label_color: DARK_BLUE, point_size: 5, @@ -502,36 +519,36 @@ impl Graph for CallButterfly { points.push(ChartPoint { coordinates: ( - self.short_call.option.strike_price.value(), - max_profit.value(), + self.long_call.option.strike_price.value(), + self.calculate_profit_at(self.long_call.option.strike_price), ), - label: format!("Max Profit\n\n{:.2}", max_profit), - label_offset: LabelOffsetType::Relative(2.0, 1.0), - point_color: DARK_GREEN, - label_color: DARK_GREEN, + label: format!("Left Loss\n\n{:.2}", max_profit), + label_offset: LabelOffsetType::Relative(3.0, 3.0), + point_color: RED, + label_color: RED, point_size: 5, font_size: 18, }); - let lower_loss = self.calculate_profit_at(self.long_call_itm.option.strike_price); - let upper_loss = self.calculate_profit_at(self.long_call_otm.option.strike_price); + let lower_loss = self.calculate_profit_at(self.short_call_low.option.strike_price); + let upper_loss = self.calculate_profit_at(self.short_call_high.option.strike_price); points.push(ChartPoint { - coordinates: (self.long_call_itm.option.strike_price.value(), lower_loss), - label: format!("Left Low {:.2}", lower_loss), - label_offset: LabelOffsetType::Relative(0.0, -1.0), - point_color: RED, - label_color: RED, + coordinates: (self.short_call_low.option.strike_price.value(), lower_loss), + label: format!("Left High {:.2}", lower_loss), + label_offset: LabelOffsetType::Relative(3.0, -3.0), + point_color: DARK_GREEN, + label_color: DARK_GREEN, point_size: 5, font_size: 18, }); points.push(ChartPoint { - coordinates: (self.long_call_otm.option.strike_price.value(), upper_loss), - label: format!("Right Low {:.2}", upper_loss), - label_offset: LabelOffsetType::Relative(-18.0, -1.0), - point_color: RED, - label_color: RED, + coordinates: (self.short_call_high.option.strike_price.value(), upper_loss), + label: format!("Right High {:.2}", upper_loss), + label_offset: LabelOffsetType::Relative(3.0, 3.0), + point_color: DARK_GREEN, + label_color: DARK_GREEN, point_size: 5, font_size: 18, }); @@ -544,69 +561,69 @@ impl Graph for CallButterfly { impl Greeks for CallButterfly { fn greeks(&self) -> Greek { - let long_call_itm_greek = self.long_call_itm.greeks(); - let long_call_otm_greek = self.long_call_otm.greeks(); - let short_call_greek = self.short_call.greeks(); + let short_call_low_greek = self.short_call_low.greeks(); + let short_call_high_greek = self.short_call_high.greeks(); + let long_call_greek = self.long_call.greeks(); Greek { - delta: long_call_itm_greek.delta + long_call_otm_greek.delta + short_call_greek.delta, - gamma: long_call_itm_greek.gamma + long_call_otm_greek.gamma + short_call_greek.gamma, - theta: long_call_itm_greek.theta + long_call_otm_greek.theta + short_call_greek.theta, - vega: long_call_itm_greek.vega + long_call_otm_greek.vega + short_call_greek.vega, - rho: long_call_itm_greek.rho + long_call_otm_greek.rho + short_call_greek.rho, - rho_d: long_call_itm_greek.rho_d + long_call_otm_greek.rho_d + short_call_greek.rho_d, + delta: short_call_low_greek.delta + short_call_high_greek.delta + long_call_greek.delta, + gamma: short_call_low_greek.gamma + short_call_high_greek.gamma + long_call_greek.gamma, + theta: short_call_low_greek.theta + short_call_high_greek.theta + long_call_greek.theta, + vega: short_call_low_greek.vega + short_call_high_greek.vega + long_call_greek.vega, + rho: short_call_low_greek.rho + short_call_high_greek.rho + long_call_greek.rho, + rho_d: short_call_low_greek.rho_d + short_call_high_greek.rho_d + long_call_greek.rho_d, } } } impl DeltaNeutrality for CallButterfly { fn calculate_net_delta(&self) -> DeltaInfo { - let long_call_itm_delta = self.long_call_itm.option.delta(); - let long_call_otm_delta = self.long_call_otm.option.delta(); - let short_call_delta = self.short_call.option.delta(); + let long_call_itm_delta = self.short_call_low.option.delta(); + let long_call_otm_delta = self.short_call_high.option.delta(); + let short_call_delta = self.long_call.option.delta(); let threshold = DELTA_THRESHOLD; let delta = long_call_itm_delta + long_call_otm_delta + short_call_delta; DeltaInfo { net_delta: delta, individual_deltas: vec![long_call_itm_delta, long_call_otm_delta, short_call_delta], is_neutral: (delta).abs() < threshold, - underlying_price: self.long_call_itm.option.underlying_price, + underlying_price: self.short_call_low.option.underlying_price, neutrality_threshold: threshold, } } fn get_atm_strike(&self) -> PositiveF64 { - self.long_call_itm.option.underlying_price + self.short_call_low.option.underlying_price } fn generate_delta_reducing_adjustments(&self) -> Vec { let net_delta = self.calculate_net_delta().net_delta; - vec![DeltaAdjustment::SellOptions { - quantity: pos!((net_delta.abs() / self.short_call.option.delta()).abs()) - * self.short_call.option.quantity, - strike: self.short_call.option.strike_price, - option_type: OptionStyle::Call, - }] - } - - fn generate_delta_increasing_adjustments(&self) -> Vec { - let net_delta = self.calculate_net_delta().net_delta; - vec![ - DeltaAdjustment::BuyOptions { - quantity: pos!((net_delta.abs() / self.long_call_itm.option.delta()).abs()) - * self.long_call_itm.option.quantity, - strike: self.long_call_itm.option.strike_price, + DeltaAdjustment::SellOptions { + quantity: pos!((net_delta.abs() / self.short_call_low.option.delta()).abs()) + * self.short_call_low.option.quantity, + strike: self.short_call_low.option.strike_price, option_type: OptionStyle::Call, }, - DeltaAdjustment::BuyOptions { - quantity: pos!((net_delta.abs() / self.long_call_otm.option.delta()).abs()) - * self.long_call_otm.option.quantity, - strike: self.long_call_otm.option.strike_price, + DeltaAdjustment::SellOptions { + quantity: pos!((net_delta.abs() / self.short_call_high.option.delta()).abs()) + * self.short_call_high.option.quantity, + strike: self.short_call_high.option.strike_price, option_type: OptionStyle::Call, }, ] } + + fn generate_delta_increasing_adjustments(&self) -> Vec { + let net_delta = self.calculate_net_delta().net_delta; + + vec![DeltaAdjustment::BuyOptions { + quantity: pos!((net_delta.abs() / self.long_call.option.delta()).abs()) + * self.long_call.option.quantity, + strike: self.long_call.option.strike_price, + option_type: OptionStyle::Call, + }] + } } #[cfg(test)] @@ -627,10 +644,11 @@ mod tests_call_butterfly { 0.01, 0.02, pos!(1.0), - pos!(2.0), + 45.0, 30.0, 20.5, - 20.0, + 0.1, + 0.1, 0.1, 0.1, 0.1, @@ -651,13 +669,13 @@ mod tests_call_butterfly { #[test] fn test_break_even() { let strategy = setup(); - assert_eq!(strategy.break_even()[0], 166.3); + assert_eq!(strategy.break_even()[0], 150.1); } #[test] fn test_calculate_profit_at() { let strategy = setup(); - let price = 157.0; + let price = 172.0; assert!(strategy.calculate_profit_at(pos!(price)) < ZERO); } @@ -667,28 +685,16 @@ mod tests_call_butterfly { assert!(strategy.max_profit().unwrap_or(PZERO) > PZERO); } - #[test] - fn test_max_loss() { - let strategy = setup(); - assert_eq!(strategy.max_loss().unwrap_or(PZERO), strategy.total_cost()); - } - - #[test] - fn test_total_cost() { - let strategy = setup(); - assert!(strategy.total_cost() > PZERO); - } - #[test] fn test_net_premium_received() { let strategy = setup(); - assert_eq!(strategy.net_premium_received(), 39.6); + assert_relative_eq!(strategy.net_premium_received(), 4.8999, epsilon = 0.0001); } #[test] fn test_fees() { let strategy = setup(); - assert_relative_eq!(strategy.fees(), 0.8, epsilon = f64::EPSILON); + assert_relative_eq!(strategy.fees(), 0.6, epsilon = f64::EPSILON); } #[test] @@ -726,14 +732,14 @@ mod tests_call_butterfly_validation { "AAPL".to_string(), pos!(150.0), pos!(145.0), - pos!(155.0), pos!(150.0), + pos!(155.0), ExpirationDate::Days(30.0), 0.2, 0.01, 0.02, pos!(1.0), - pos!(2.0), + 7.0, 5.0, 3.0, 4.0, @@ -741,6 +747,7 @@ mod tests_call_butterfly_validation { 0.1, 0.1, 0.1, + 0.1, ) } @@ -758,13 +765,6 @@ mod tests_call_butterfly_validation { assert!(!strategy.validate()); } - #[test] - fn test_validate_invalid_quantity_ratio() { - let mut strategy = setup_basic_strategy(); - strategy.short_call.option.quantity = pos!(1.0); // Should be 2x long quantity - assert!(!strategy.validate()); - } - #[test] fn test_validate_valid_strategy() { let strategy = setup_basic_strategy(); @@ -772,104 +772,6 @@ mod tests_call_butterfly_validation { } } -#[cfg(test)] -mod tests_call_butterfly_optimization { - use super::*; - use crate::spos; - - fn create_test_option_chain() -> OptionChain { - let mut chain = OptionChain::new("AAPL", pos!(150.0), "2024-01-01".to_string()); - - // Add options at various strikes - for strike in [145.0, 147.5, 150.0, 152.5, 155.0].iter() { - chain.add_option( - pos!(*strike), - spos!(3.0), - spos!(3.2), - spos!(2.8), - spos!(3.0), - spos!(0.2), - Some(0.5), - spos!(100.0), - Some(50), - ); - } - chain - } - - #[test] - fn test_is_valid_short_option_upper() { - let strategy = CallButterfly::default(); - let option = OptionData::new( - pos!(155.0), - spos!(3.0), - spos!(3.2), - spos!(2.8), - spos!(3.0), - spos!(0.2), - None, - None, - None, - ); - assert!(strategy.is_valid_short_option(&option, &FindOptimalSide::Upper)); - } - - #[test] - fn test_are_valid_prices() { - let strategy = CallButterfly::default(); - let long_itm = OptionData::new( - pos!(145.0), - spos!(3.0), - spos!(3.2), - spos!(2.8), - spos!(3.0), - spos!(0.2), - None, - None, - None, - ); - let long_otm = OptionData::new( - pos!(155.0), - spos!(3.0), - spos!(3.2), - spos!(2.8), - spos!(3.0), - spos!(0.2), - None, - None, - None, - ); - let short = OptionData::new( - pos!(150.0), - spos!(3.0), - spos!(3.2), - spos!(2.8), - spos!(3.0), - spos!(0.2), - None, - None, - None, - ); - assert!(strategy.are_valid_prices(&long_itm, &long_otm, &short)); - } - - #[test] - fn test_find_optimal_ratio() { - let mut strategy = CallButterfly::default(); - let chain = create_test_option_chain(); - strategy.find_optimal(&chain, FindOptimalSide::All, OptimizationCriteria::Ratio); - assert!(strategy.validate()); - } - - #[test] - fn test_find_optimal_area() { - let mut strategy = CallButterfly::default(); - let chain = create_test_option_chain(); - strategy.find_optimal(&chain, FindOptimalSide::All, OptimizationCriteria::Area); - assert!(strategy.validate()); - } -} - #[cfg(test)] mod tests_call_butterfly_pnl { use super::*; @@ -879,14 +781,14 @@ mod tests_call_butterfly_pnl { "AAPL".to_string(), pos!(150.0), pos!(145.0), - pos!(155.0), pos!(150.0), + pos!(155.0), ExpirationDate::Days(30.0), 0.2, 0.01, 0.02, pos!(1.0), - pos!(2.0), + 7.0, 5.0, 3.0, 4.0, @@ -894,16 +796,10 @@ mod tests_call_butterfly_pnl { 0.1, 0.1, 0.1, + 0.1, ) } - #[test] - fn test_profit_at_max_point() { - let strategy = setup_test_strategy(); - let profit = strategy.calculate_profit_at(strategy.short_call.option.strike_price); - assert_eq!(profit, strategy.max_profit().unwrap_or(PZERO).value()); - } - #[test] fn test_profit_below_lower_strike() { let strategy = setup_test_strategy(); @@ -924,18 +820,13 @@ mod tests_call_butterfly_pnl { let ratio = strategy.profit_ratio(); assert!(ratio > 0.0); } - - #[test] - fn test_profit_area() { - let strategy = setup_test_strategy(); - let area = strategy.profit_area(); - assert!(area > 0.0); - } } #[cfg(test)] mod tests_call_butterfly_graph { use super::*; + use crate::model::types::ExpirationDate; + use approx::assert_relative_eq; fn setup_test_strategy() -> CallButterfly { CallButterfly::new( @@ -949,7 +840,7 @@ mod tests_call_butterfly_graph { 0.01, 0.02, pos!(1.0), - pos!(2.0), + 7.0, 5.0, 3.0, 4.0, @@ -957,39 +848,111 @@ mod tests_call_butterfly_graph { 0.1, 0.1, 0.1, + 0.1, ) } #[test] - fn test_get_points() { - let strategy = setup_test_strategy(); - let points = strategy.get_points(); - assert!(!points.is_empty()); + fn test_vertical_lines() { + let butterfly = setup_test_strategy(); + let lines = butterfly.get_vertical_lines(); - let has_break_even = points.iter().any(|p| p.label.contains("Break Even")); - let has_max_profit = points.iter().any(|p| p.label.contains("Max Profit")); - let has_low_point = points.iter().any(|p| p.label.contains("Low")); + assert_eq!(lines.len(), 1, "Should have exactly one vertical line"); - assert!(has_break_even); - assert!(has_max_profit); - assert!(has_low_point); + let line = &lines[0]; + assert_relative_eq!(line.x_coordinate, 150.0, epsilon = 0.001); + assert_eq!(line.y_range, (f64::NEG_INFINITY, f64::INFINITY)); + assert!(line.label.contains("Current Price: 150.00")); + assert_eq!(line.font_size, 18); } #[test] - fn test_get_vertical_lines() { - let strategy = setup_test_strategy(); - let lines = strategy.get_vertical_lines(); - assert_eq!(lines.len(), 1); - assert!(lines[0].label.contains("Current Price")); + fn test_points_count_and_labels() { + let butterfly = setup_test_strategy(); + let points = butterfly.get_points(); + + // Should have 6 points: 2 break-even, 1 left loss, 2 max profit points, and current price + assert_eq!(points.len(), 6, "Should have exactly 6 points"); + + let labels: Vec<&str> = points.iter().map(|p| p.label.as_str()).collect(); + + // Verify all required labels are present + assert!(labels.iter().any(|&l| l.contains("Low Break Even"))); + assert!(labels.iter().any(|&l| l.contains("High Break Even"))); + assert!(labels.iter().any(|&l| l.contains("Left Loss"))); + assert!(labels.iter().any(|&l| l.contains("Left High"))); + assert!(labels.iter().any(|&l| l.contains("Right High"))); } #[test] - fn test_best_range_to_show() { - let strategy = setup_test_strategy(); - let range = strategy.best_range_to_show(pos!(5.0)).unwrap(); - assert!(!range.is_empty()); - assert!(range[0] < strategy.long_call_itm.option.strike_price); - assert!(range[range.len() - 1] > strategy.long_call_otm.option.strike_price); + fn test_points_coordinates() { + let butterfly = setup_test_strategy(); + let points = butterfly.get_points(); + + // Check for points at strike prices + let strike_coordinates: Vec = points.iter().map(|p| p.coordinates.0).collect(); + + assert!( + strike_coordinates + .iter() + .any(|&x| (x - 145.0).abs() < 0.001), + "Missing point near 145.0 strike" + ); + assert!( + strike_coordinates + .iter() + .any(|&x| (x - 150.0).abs() < 0.001), + "Missing point near 150.0 strike" + ); + assert!( + strike_coordinates + .iter() + .any(|&x| (x - 155.0).abs() < 0.001), + "Missing point near 155.0 strike" + ); + } + + #[test] + fn test_point_styling() { + let butterfly = setup_test_strategy(); + let points = butterfly.get_points(); + + for point in points { + assert_eq!(point.point_size, 5, "Point size should be 5"); + assert_eq!(point.font_size, 18, "Font size should be 18"); + + // Verify label offset is set + match point.label_offset { + LabelOffsetType::Relative(x, y) => { + assert!(x != 0.0, "X offset should not be 0"); + assert!(y != 0.0, "Y offset should not be 0"); + } + _ => panic!("Expected Relative label offset"), + } + } + } + + #[test] + fn test_break_even_points() { + let butterfly = setup_test_strategy(); + let points = butterfly.get_points(); + + // Find break-even points + let break_even_points: Vec<&ChartPoint<(f64, f64)>> = points + .iter() + .filter(|p| p.label.contains("Break Even")) + .collect(); + + assert_eq!( + break_even_points.len(), + 2, + "Should have exactly 2 break-even points" + ); + + // Verify break-even points have y-coordinate = 0 + for point in break_even_points { + assert_relative_eq!(point.coordinates.1, 0.0, epsilon = 0.001); + } } } @@ -1014,7 +977,7 @@ mod tests_iron_condor_delta { 0.05, // risk_free_rate 0.0, // dividend_yield pos!(1.0), // long quantity - pos!(2.0), // short_quantity + 95.8, // short_quantity 85.04, // premium_long_itm 31.65, // premium_long_otm 53.04, // premium_short @@ -1022,6 +985,7 @@ mod tests_iron_condor_delta { 0.78, // close_fee_long 0.73, // close_fee_short 0.73, // close_fee_short + 0.73, // close_fee_short ) } @@ -1031,7 +995,7 @@ mod tests_iron_condor_delta { assert_relative_eq!( strategy.calculate_net_delta().net_delta, - -0.086598, + -0.687410, epsilon = 0.0001 ); assert!(!strategy.is_delta_neutral()); @@ -1039,23 +1003,15 @@ mod tests_iron_condor_delta { assert_eq!( suggestion[0], DeltaAdjustment::BuyOptions { - quantity: pos!(0.08869424268674732), + quantity: pos!(0.7040502965074416), strike: pos!(5750.0), option_type: OptionStyle::Call } ); - assert_eq!( - suggestion[1], - DeltaAdjustment::BuyOptions { - quantity: pos!(0.11472018606079874), - strike: pos!(5850.0), - option_type: OptionStyle::Call - } - ); - let mut option = strategy.long_call_itm.option.clone(); - option.quantity = pos!(0.08869424268674732); - assert_relative_eq!(option.delta(), 0.086598, epsilon = 0.0001); + let mut option = strategy.long_call.option.clone(); + option.quantity = pos!(0.7040502965074416); + assert_relative_eq!(option.delta(), 0.687410, epsilon = 0.0001); assert_relative_eq!( option.delta() + strategy.calculate_net_delta().net_delta, 0.0, @@ -1069,7 +1025,7 @@ mod tests_iron_condor_delta { assert_relative_eq!( strategy.calculate_net_delta().net_delta, - 0.032444, + 0.055904, epsilon = 0.0001 ); assert!(!strategy.is_delta_neutral()); @@ -1077,15 +1033,23 @@ mod tests_iron_condor_delta { assert_eq!( suggestion[0], DeltaAdjustment::SellOptions { - quantity: pos!(0.07766273391000812), + quantity: pos!(0.2835618144021529), + strike: pos!(5850.0), + option_type: OptionStyle::Call + } + ); + assert_eq!( + suggestion[1], + DeltaAdjustment::SellOptions { + quantity: pos!(0.1338190182607821), strike: pos!(5800.0), option_type: OptionStyle::Call } ); - let mut option = strategy.short_call.option.clone(); - option.quantity = pos!(0.07766273391000812); - assert_relative_eq!(option.delta(), -0.032444, epsilon = 0.0001); + let mut option = strategy.short_call_low.option.clone(); + option.quantity = pos!(0.2835618144021529); + assert_relative_eq!(option.delta(), -0.055904, epsilon = 0.0001); assert_relative_eq!( option.delta() + strategy.calculate_net_delta().net_delta, 0.0, @@ -1095,7 +1059,7 @@ mod tests_iron_condor_delta { #[test] fn create_test_no_adjustments() { - let strategy = get_strategy(pos!(5800.0)); + let strategy = get_strategy(pos!(5795.0)); assert_relative_eq!( strategy.calculate_net_delta().net_delta, @@ -1129,7 +1093,7 @@ mod tests_iron_condor_delta_size { 0.05, // risk_free_rate 0.0, // dividend_yield pos!(1.0), // long quantity - pos!(2.0), // short_quantity + 97.8, // short_quantity 85.04, // premium_long_itm 31.65, // premium_long_otm 53.04, // premium_short @@ -1137,6 +1101,7 @@ mod tests_iron_condor_delta_size { 0.78, // close_fee_long 0.73, // close_fee_short 0.73, // close_fee_short + 0.73, ) } @@ -1146,7 +1111,7 @@ mod tests_iron_condor_delta_size { assert_relative_eq!( strategy.calculate_net_delta().net_delta, - -0.0931943, + -0.5699325, epsilon = 0.0001 ); assert!(!strategy.is_delta_neutral()); @@ -1154,23 +1119,15 @@ mod tests_iron_condor_delta_size { assert_eq!( suggestion[0], DeltaAdjustment::BuyOptions { - quantity: pos!(0.09726918791103065), + quantity: pos!(0.5948524360242091), strike: pos!(5750.0), option_type: OptionStyle::Call } ); - assert_eq!( - suggestion[1], - DeltaAdjustment::BuyOptions { - quantity: pos!(0.13945831929041605), - strike: pos!(5850.0), - option_type: OptionStyle::Call - } - ); - let mut option = strategy.long_call_otm.option.clone(); - option.quantity = pos!(0.13945831929041605); - assert_relative_eq!(option.delta(), 0.09319, epsilon = 0.0001); + let mut option = strategy.long_call.option.clone(); + option.quantity = pos!(0.5948524360242091); + assert_relative_eq!(option.delta(), 0.56993, epsilon = 0.0001); assert_relative_eq!( option.delta() + strategy.calculate_net_delta().net_delta, 0.0, @@ -1184,7 +1141,7 @@ mod tests_iron_condor_delta_size { assert_relative_eq!( strategy.calculate_net_delta().net_delta, - 0.03244, + 0.05590, epsilon = 0.0001 ); assert!(!strategy.is_delta_neutral()); @@ -1193,15 +1150,23 @@ mod tests_iron_condor_delta_size { assert_eq!( suggestion[0], DeltaAdjustment::SellOptions { - quantity: pos!(0.07766273391000812), + quantity: pos!(0.2835618144021529), + strike: pos!(5850.0), + option_type: OptionStyle::Call + } + ); + assert_eq!( + suggestion[1], + DeltaAdjustment::SellOptions { + quantity: pos!(0.1338190182607821), strike: pos!(5800.0), option_type: OptionStyle::Call } ); - let mut option = strategy.short_call.option.clone(); - option.quantity = pos!(0.07766273391000812); - assert_relative_eq!(option.delta(), -0.032444, epsilon = 0.0001); + let mut option = strategy.short_call_low.option.clone(); + option.quantity = pos!(0.2835618144021529); + assert_relative_eq!(option.delta(), -0.05590, epsilon = 0.0001); assert_relative_eq!( option.delta() + strategy.calculate_net_delta().net_delta, 0.0, @@ -1211,7 +1176,7 @@ mod tests_iron_condor_delta_size { #[test] fn create_test_no_adjustments() { - let strategy = get_strategy(pos!(5800.0)); + let strategy = get_strategy(pos!(5795.0)); assert_relative_eq!( strategy.calculate_net_delta().net_delta, @@ -1223,3 +1188,181 @@ mod tests_iron_condor_delta_size { assert_eq!(suggestion[0], DeltaAdjustment::NoAdjustmentNeeded); } } + +#[cfg(test)] +mod tests_call_butterfly_optimizable { + use super::*; + use approx::assert_relative_eq; + + fn create_test_option_chain() -> OptionChain { + let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-12-19".to_string(), None, None); + + // Add options with different strikes + chain.add_option( + pos!(95.0), // strike + spos!(6.0), // call_bid + spos!(6.2), // call_ask + spos!(1.0), // put_bid + spos!(1.2), // put_ask + spos!(0.2), // iv + Some(0.4), // delta + spos!(100.0),// volume + Some(50), // open interest + ); + + chain.add_option( + pos!(100.0), + spos!(3.0), + spos!(3.2), + spos!(3.0), + spos!(3.2), + spos!(0.2), + Some(0.5), + spos!(200.0), + Some(100), + ); + + chain.add_option( + pos!(105.0), + spos!(1.0), + spos!(1.2), + spos!(6.0), + spos!(6.2), + spos!(0.2), + Some(0.6), + spos!(100.0), + Some(50), + ); + + chain + } + + fn setup_test_butterfly() -> CallButterfly { + CallButterfly::new( + "TEST".to_string(), + pos!(100.0), + pos!(95.0), + pos!(100.0), + pos!(105.0), + ExpirationDate::Days(30.0), + 0.2, + 0.01, + 0.02, + pos!(1.0), + 6.2, // long call ask + 3.0, // short call bid low + 1.0, // short call bid high + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + ) + } + + #[test] + fn test_filter_combinations() { + let butterfly = setup_test_butterfly(); + let chain = create_test_option_chain(); + + let combinations: Vec<_> = butterfly + .filter_combinations(&chain, FindOptimalSide::All) + .collect(); + + assert!(!combinations.is_empty(), "Should find valid combinations"); + + // Test first combination + if let OptionDataGroup::Three(long, short_low, short_high) = &combinations[0] { + assert!(long.strike_price < short_low.strike_price); + assert!(short_low.strike_price < short_high.strike_price); + assert!(long.call_ask.is_some()); + assert!(short_low.call_bid.is_some()); + assert!(short_high.call_bid.is_some()); + } else { + panic!("Expected Three legs in OptionDataGroup"); + } + } + + #[test] + fn test_find_optimal_ratio() { + let mut butterfly = setup_test_butterfly(); + let chain = create_test_option_chain(); + + butterfly.find_optimal(&chain, FindOptimalSide::All, OptimizationCriteria::Ratio); + + // Verify the optimization resulted in valid strikes + assert!(butterfly.long_call.option.strike_price < butterfly.short_call_low.option.strike_price); + assert!(butterfly.short_call_low.option.strike_price < butterfly.short_call_high.option.strike_price); + + // Verify the strategy is valid + assert!(butterfly.validate()); + assert!(butterfly.max_profit().is_ok()); + assert!(butterfly.max_loss().is_ok()); + } + + #[test] + fn test_find_optimal_area() { + let mut butterfly = setup_test_butterfly(); + let chain = create_test_option_chain(); + + butterfly.find_optimal(&chain, FindOptimalSide::All, OptimizationCriteria::Area); + + // Verify the optimization resulted in valid strikes + assert!(butterfly.long_call.option.strike_price < butterfly.short_call_low.option.strike_price); + assert!(butterfly.short_call_low.option.strike_price < butterfly.short_call_high.option.strike_price); + + // Verify the strategy is valid + assert!(butterfly.validate()); + assert!(butterfly.max_profit().is_ok()); + assert!(butterfly.max_loss().is_ok()); + } + + #[test] + fn test_create_strategy() { + let butterfly = setup_test_butterfly(); + let chain = create_test_option_chain(); + + let legs = StrategyLegs::ThreeLegs { + first: chain.options.iter().next().unwrap(), + second: chain.options.iter().nth(1).unwrap(), + third: chain.options.iter().nth(2).unwrap(), + }; + + let new_strategy = butterfly.create_strategy(&chain, &legs); + + // Verify the new strategy has correct properties + assert_relative_eq!( + new_strategy.get_underlying_price().value(), + 100.0, + epsilon = 0.001 + ); + assert!(new_strategy.validate()); + } + + #[test] + #[should_panic(expected = "Invalid number of legs for this strategy")] + fn test_create_strategy_invalid_legs() { + let butterfly = setup_test_butterfly(); + let chain = create_test_option_chain(); + + let legs = StrategyLegs::TwoLegs { + first: chain.options.iter().next().unwrap(), + second: chain.options.iter().nth(1).unwrap(), + }; + + butterfly.create_strategy(&chain, &legs); // Should panic + } + + #[test] + fn test_filter_combinations_empty_chain() { + let butterfly = setup_test_butterfly(); + let empty_chain = OptionChain::new("TEST", pos!(100.0), "2024-12-19".to_string(), None, None); + + let combinations: Vec<_> = butterfly + .filter_combinations(&empty_chain, FindOptimalSide::All) + .collect(); + + assert!(combinations.is_empty(), "Empty chain should yield no combinations"); + } +} \ No newline at end of file diff --git a/src/strategies/delta_neutral/mod.rs b/src/strategies/delta_neutral/mod.rs index 7609f0d5..857ed411 100644 --- a/src/strategies/delta_neutral/mod.rs +++ b/src/strategies/delta_neutral/mod.rs @@ -34,6 +34,7 @@ //! ## Example Usage //! //! ```rust +//! use tracing::info; //! use optionstratlib::greeks::equations::{Greek, Greeks}; //! use optionstratlib::model::types::PositiveF64; //! use optionstratlib::pos; @@ -62,13 +63,13 @@ //! //! // Calculate net delta //! let delta_info = strategy.calculate_net_delta(); -//! println!("{}", delta_info); +//! info!("{}", delta_info); //! //! // Check delta-neutrality within a 0.1 threshold //! if !strategy.is_delta_neutral() { //! let adjustments = strategy.suggest_delta_adjustments(); //! for adj in adjustments { -//! println!("{:?}", adj); +//! info!("{:?}", adj); //! } //! } //! ``` diff --git a/src/strategies/iron_butterfly.rs b/src/strategies/iron_butterfly.rs index 3e70364f..5cbfa902 100644 --- a/src/strategies/iron_butterfly.rs +++ b/src/strategies/iron_butterfly.rs @@ -12,7 +12,8 @@ Key characteristics: - Requires very low volatility */ use super::base::{Optimizable, Positionable, Strategies, StrategyType, Validable}; -use crate::chains::chain::{OptionChain, OptionData}; +use crate::chains::chain::OptionChain; +use crate::chains::utils::OptionDataGroup; use crate::chains::StrategyLegs; use crate::constants::{DARK_BLUE, DARK_GREEN, ZERO}; use crate::greeks::equations::{Greek, Greeks}; @@ -30,7 +31,7 @@ use crate::visualization::utils::Graph; use chrono::Utc; use plotters::prelude::full_palette::ORANGE; use plotters::prelude::{ShapeStyle, RED}; -use tracing::{debug, error}; +use tracing::{error, info}; const IRON_BUTTERFLY_DESCRIPTION: &str = "An Iron Butterfly is a neutral options strategy combining selling an at-the-money put and call \ @@ -336,6 +337,42 @@ impl Strategies for IronButterfly { impl Optimizable for IronButterfly { type Strategy = IronButterfly; + fn filter_combinations<'a>( + &'a self, + option_chain: &'a OptionChain, + side: FindOptimalSide, + ) -> impl Iterator> { + let underlying_price = self.get_underlying_price(); + let strategy = self.clone(); + option_chain + .get_triple_iter() + // Filter out invalid combinations based on FindOptimalSide + .filter(move |(low, mid, high)| { + low.is_valid_optimal_side(underlying_price, &side) + && mid.is_valid_optimal_side(underlying_price, &side) + && high.is_valid_optimal_side(underlying_price, &side) + }) + // Filter out options with invalid bid/ask prices + .filter(|(low, mid, high)| { + low.put_ask.unwrap_or(PZERO) > PZERO + && mid.put_bid.unwrap_or(PZERO) > PZERO + && high.call_ask.unwrap_or(PZERO) > PZERO + }) + // Filter out options that don't meet strategy constraints + .filter(move |(low, mid, high)| { + let legs = StrategyLegs::FourLegs { + first: low, + second: mid, + third: mid, + fourth: high, + }; + let strategy = strategy.create_strategy(option_chain, &legs); + strategy.validate() && strategy.max_profit().is_ok() && strategy.max_loss().is_ok() + }) + // Map to OptionDataGroup + .map(move |(low, mid, high)| OptionDataGroup::Three(low, mid, high)) + } + fn find_optimal( &mut self, option_chain: &OptionChain, @@ -343,123 +380,43 @@ impl Optimizable for IronButterfly { criteria: OptimizationCriteria, ) { let mut best_value = f64::NEG_INFINITY; - let options: Vec<&OptionData> = option_chain.options.iter().collect(); - - for (i, long_put) in options.iter().enumerate() { - if !self.is_valid_long_option(long_put, &side) { - continue; - } - - for (j, short_strike) in options.iter().enumerate().skip(i + 1) { - if !self.is_valid_short_option(short_strike, &side) { - continue; - } - - for long_call in options.iter().skip(j + 1) { - if !self.is_valid_long_option(long_call, &side) { - continue; - } - - if long_put.strike_price >= short_strike.strike_price - || short_strike.strike_price >= long_call.strike_price - { - error!("Invalid order of strikes"); - continue; - } - - if !self.are_valid_prices(&StrategyLegs::FourLegs { - first: long_put, - second: short_strike, - third: short_strike, - fourth: long_call, - }) { - error!("Invalid prices"); - continue; - } - - let new_strategy = self.create_strategy( - option_chain, - &StrategyLegs::FourLegs { - first: long_put, - second: short_strike, - third: short_strike, - fourth: long_call, - }, - ); - - if !new_strategy.validate() { - debug!("Invalid strategy"); - continue; - } - - if new_strategy.max_profit().is_err() || new_strategy.max_loss().is_err() { - debug!("Invalid profit or loss"); - continue; - } - - let current_value = match criteria { - OptimizationCriteria::Ratio => new_strategy.profit_ratio(), - OptimizationCriteria::Area => new_strategy.profit_area(), - }; - - if current_value > best_value { - debug!("New best value: {}", current_value); - best_value = current_value; - *self = new_strategy; - } - } + let strategy_clone = self.clone(); + let options_iter = strategy_clone.filter_combinations(option_chain, side); + + for option_data_group in options_iter { + // Unpack the OptionDataGroup into individual options + let (low, mid, high) = match option_data_group { + OptionDataGroup::Three(first, second, third) => (first, second, third), + _ => panic!("Invalid OptionDataGroup"), + }; + + let legs = StrategyLegs::FourLegs { + first: low, + second: mid, + third: mid, + fourth: high, + }; + let strategy = self.create_strategy(option_chain, &legs); + // Calculate the current value based on the optimization criteria + let current_value = match criteria { + OptimizationCriteria::Ratio => strategy.profit_ratio(), + OptimizationCriteria::Area => strategy.profit_area(), + }; + + if current_value > best_value { + // Update the best value and replace the current strategy + info!("Found better value: {}", current_value); + best_value = current_value; + *self = strategy.clone(); } } } - fn is_valid_short_option(&self, option: &OptionData, side: &FindOptimalSide) -> bool { - let is_valid_strike = match side { - FindOptimalSide::Upper => option.strike_price >= self.get_underlying_price(), - FindOptimalSide::Lower => option.strike_price <= self.get_underlying_price(), - FindOptimalSide::All => true, - FindOptimalSide::Range(start, end) => { - option.strike_price >= *start && option.strike_price <= *end - } - }; - is_valid_strike - && option.put_bid.unwrap_or(PZERO) > PZERO - && option.call_bid.unwrap_or(PZERO) > PZERO - // TODO: review this - } - - fn is_valid_long_option(&self, option: &OptionData, side: &FindOptimalSide) -> bool { - let is_valid_strike = match side { - FindOptimalSide::Upper => option.strike_price >= self.get_underlying_price(), - FindOptimalSide::Lower => option.strike_price <= self.get_underlying_price(), - FindOptimalSide::All => true, - FindOptimalSide::Range(start, end) => { - option.strike_price >= *start && option.strike_price <= *end - } - }; - is_valid_strike - && option.put_ask.unwrap_or(PZERO) > PZERO - && option.call_ask.unwrap_or(PZERO) > PZERO - // TODO: review this - } - - fn are_valid_prices(&self, legs: &StrategyLegs) -> bool { - match legs { - StrategyLegs::FourLegs { - first: long_put, - second: short_put, - third: short_call, - fourth: long_call, - } => { - long_put.put_ask.unwrap_or(PZERO) > PZERO - && short_put.put_bid.unwrap_or(PZERO) > PZERO - && short_call.call_bid.unwrap_or(PZERO) > PZERO - && long_call.call_ask.unwrap_or(PZERO) > PZERO - } - _ => false, - } - } - fn create_strategy(&self, chain: &OptionChain, legs: &StrategyLegs) -> Self::Strategy { + // self.option.strike_price < self.short_put.option.strike_price + // && self.short_put.option.strike_price == self.short_call.option.strike_price + // && self.short_call.option.strike_price < self.long_call.option.strike_price + match legs { StrategyLegs::FourLegs { first: long_put, @@ -510,7 +467,10 @@ impl Graph for IronButterfly { format!("Short Put: ${}", self.short_put.option.strike_price), format!("Short Call: ${}", self.short_call.option.strike_price), format!("Long Call: ${}", self.long_call.option.strike_price), - format!("Expire: {}", self.short_put.option.expiration_date), + format!( + "Expire: {}", + self.short_put.option.expiration_date.get_date_string() + ), ] .iter() .map(|leg| leg.to_string()) @@ -1263,6 +1223,7 @@ mod tests_iron_butterfly_strategies { #[cfg(test)] mod tests_iron_butterfly_optimizable { use super::*; + use crate::chains::chain::OptionData; use crate::model::types::ExpirationDate; use crate::pos; use crate::spos; @@ -1289,7 +1250,7 @@ mod tests_iron_butterfly_optimizable { } fn create_test_chain() -> OptionChain { - let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-12-31".to_string()); + let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-12-31".to_string(), None, None); // Add options at various strikes for strike in [85.0, 90.0, 95.0, 100.0, 105.0, 110.0, 115.0] { @@ -1407,23 +1368,6 @@ mod tests_iron_butterfly_optimizable { .is_valid_short_option(&option, &FindOptimalSide::Range(pos!(95.0), pos!(105.0)))); } - #[test] - fn test_are_valid_prices() { - let butterfly = create_test_butterfly(); - let chain = create_test_chain(); - let options: Vec<&OptionData> = chain.options.iter().collect(); - - // Test with same strike for short options - let legs = StrategyLegs::FourLegs { - first: options[1], // 90.0 strike for long put - second: options[3], // 100.0 strike for both shorts - third: options[3], // 100.0 strike for both shorts - fourth: options[5], // 110.0 strike for long call - }; - - assert!(butterfly.are_valid_prices(&legs)); - } - #[test] fn test_create_strategy() { let butterfly = create_test_butterfly(); diff --git a/src/strategies/iron_condor.rs b/src/strategies/iron_condor.rs index 4acff907..ee1f064f 100644 --- a/src/strategies/iron_condor.rs +++ b/src/strategies/iron_condor.rs @@ -10,7 +10,8 @@ Key characteristics: - Profit is highest when the underlying asset price remains between the two sold options at expiration */ use super::base::{Optimizable, Positionable, Strategies, StrategyType, Validable}; -use crate::chains::chain::{OptionChain, OptionData}; +use crate::chains::chain::OptionChain; +use crate::chains::utils::OptionDataGroup; use crate::chains::StrategyLegs; use crate::constants::{DARK_BLUE, DARK_GREEN, ZERO}; use crate::greeks::equations::{Greek, Greeks}; @@ -28,7 +29,7 @@ use crate::visualization::utils::Graph; use chrono::Utc; use plotters::prelude::full_palette::ORANGE; use plotters::prelude::{ShapeStyle, RED}; -use tracing::{debug, error}; +use tracing::{error, info}; const IRON_CONDOR_DESCRIPTION: &str = "An Iron Condor is a neutral options strategy combining a bull put spread with a bear call spread. \ @@ -339,6 +340,46 @@ impl Strategies for IronCondor { impl Optimizable for IronCondor { type Strategy = IronCondor; + fn filter_combinations<'a>( + &'a self, + option_chain: &'a OptionChain, + side: FindOptimalSide, + ) -> impl Iterator> { + let underlying_price = self.get_underlying_price(); + let strategy = self.clone(); + option_chain + .get_quad_iter() + // Filter out invalid combinations based on FindOptimalSide + .filter(move |(long_put, short_put, short_call, long_call)| { + long_put.is_valid_optimal_side(underlying_price, &side) + && short_put.is_valid_optimal_side(underlying_price, &side) + && short_call.is_valid_optimal_side(underlying_price, &side) + && long_call.is_valid_optimal_side(underlying_price, &side) + }) + // Filter out options with invalid bid/ask prices + .filter(|(long_put, short_put, short_call, long_call)| { + long_put.put_ask.unwrap_or(PZERO) > PZERO + && short_put.put_bid.unwrap_or(PZERO) > PZERO + && short_call.call_bid.unwrap_or(PZERO) > PZERO + && long_call.call_ask.unwrap_or(PZERO) > PZERO + }) + // Filter out options that don't meet strategy constraints + .filter(move |(long_put, short_put, short_call, long_call)| { + let legs = StrategyLegs::FourLegs { + first: long_put, + second: short_put, + third: short_call, + fourth: long_call, + }; + let strategy = strategy.create_strategy(option_chain, &legs); + strategy.validate() && strategy.max_profit().is_ok() && strategy.max_loss().is_ok() + }) + // Map to OptionDataGroup + .map(move |(long_put, short_put, short_call, long_call)| { + OptionDataGroup::Four(long_put, short_put, short_call, long_call) + }) + } + fn find_optimal( &mut self, option_chain: &OptionChain, @@ -346,124 +387,37 @@ impl Optimizable for IronCondor { criteria: OptimizationCriteria, ) { let mut best_value = f64::NEG_INFINITY; - let options: Vec<&OptionData> = option_chain.options.iter().collect(); - - for (i, long_put) in options.iter().enumerate() { - if !self.is_valid_long_option(long_put, &side) { - continue; - } - - for (j, short_put) in options.iter().enumerate().skip(i + 1) { - if !self.is_valid_short_option(short_put, &side) { - continue; - } - - for (k, short_call) in options.iter().enumerate().skip(j + 1) { - if !self.is_valid_short_option(short_call, &side) { - continue; - } - - for long_call in options.iter().skip(k + 1) { - if !self.is_valid_long_option(long_call, &side) { - continue; - } - - if long_put.strike_price >= short_put.strike_price - || short_put.strike_price >= short_call.strike_price - || short_call.strike_price >= long_call.strike_price - { - error!("Invalid order of strikes"); - continue; - } - - if !self.are_valid_prices(&StrategyLegs::FourLegs { - first: long_put, - second: short_put, - third: short_call, - fourth: long_call, - }) { - error!("Invalid prices"); - continue; - } - - let new_strategy = self.create_strategy( - option_chain, - &StrategyLegs::FourLegs { - first: long_put, - second: short_put, - third: short_call, - fourth: long_call, - }, - ); - - if !new_strategy.validate() { - debug!("Invalid strategy"); - continue; - } - - if new_strategy.max_profit().is_err() || new_strategy.max_loss().is_err() { - debug!("Invalid profit or loss"); - continue; - } - - let current_value = match criteria { - OptimizationCriteria::Ratio => new_strategy.profit_ratio(), - OptimizationCriteria::Area => new_strategy.profit_area(), - }; - - if current_value > best_value { - debug!("New best value: {}", current_value); - best_value = current_value; - *self = new_strategy; - } - } + let strategy_clone = self.clone(); + let options_iter = strategy_clone.filter_combinations(option_chain, side); + + for option_data_group in options_iter { + // Unpack the OptionDataGroup into individual options + let (long_put, short_put, short_call, long_call) = match option_data_group { + OptionDataGroup::Four(first, second, third, fourth) => { + (first, second, third, fourth) } - } - } - } + _ => panic!("Invalid OptionDataGroup"), + }; - fn is_valid_short_option(&self, option: &OptionData, side: &FindOptimalSide) -> bool { - let is_valid_strike = match side { - FindOptimalSide::Upper => option.strike_price >= self.get_underlying_price(), - FindOptimalSide::Lower => option.strike_price <= self.get_underlying_price(), - FindOptimalSide::All => true, - FindOptimalSide::Range(start, end) => { - option.strike_price >= *start && option.strike_price <= *end - } - }; - is_valid_strike - && option.put_bid.unwrap_or(PZERO) > PZERO - && option.call_bid.unwrap_or(PZERO) > PZERO - } - - fn is_valid_long_option(&self, option: &OptionData, side: &FindOptimalSide) -> bool { - let is_valid_strike = match side { - FindOptimalSide::Upper => option.strike_price >= self.get_underlying_price(), - FindOptimalSide::Lower => option.strike_price <= self.get_underlying_price(), - FindOptimalSide::All => true, - FindOptimalSide::Range(start, end) => { - option.strike_price >= *start && option.strike_price <= *end - } - }; - is_valid_strike - && option.put_ask.unwrap_or(PZERO) > PZERO - && option.call_ask.unwrap_or(PZERO) > PZERO - } - - fn are_valid_prices(&self, legs: &StrategyLegs) -> bool { - match legs { - StrategyLegs::FourLegs { + let legs = StrategyLegs::FourLegs { first: long_put, second: short_put, third: short_call, fourth: long_call, - } => { - long_put.put_ask.unwrap_or(PZERO) > PZERO - && short_put.put_bid.unwrap_or(PZERO) > PZERO - && short_call.call_bid.unwrap_or(PZERO) > PZERO - && long_call.call_ask.unwrap_or(PZERO) > PZERO + }; + let strategy = self.create_strategy(option_chain, &legs); + // Calculate the current value based on the optimization criteria + let current_value = match criteria { + OptimizationCriteria::Ratio => strategy.profit_ratio(), + OptimizationCriteria::Area => strategy.profit_area(), + }; + + if current_value > best_value { + // Update the best value and replace the current strategy + info!("Found better value: {}", current_value); + best_value = current_value; + *self = strategy.clone(); } - _ => false, } } @@ -519,7 +473,10 @@ impl Graph for IronCondor { format!("Short Put: ${}", self.short_put.option.strike_price), format!("Short Call: ${}", self.short_call.option.strike_price), format!("Long Call: ${}", self.long_call.option.strike_price), - format!("Expire: {}", self.short_put.option.expiration_date), + format!( + "Expire: {}", + self.short_put.option.expiration_date.get_date_string() + ), ] .iter() .map(|leg| leg.to_string()) @@ -729,12 +686,6 @@ impl DeltaNeutrality for IronCondor { fn generate_delta_increasing_adjustments(&self) -> Vec { let net_delta = self.calculate_net_delta().net_delta; - - println!( - "Net Delta: {} long_call: {}", - net_delta, - self.long_call.option.delta() - ); vec![ DeltaAdjustment::BuyOptions { quantity: pos!((net_delta.abs() / self.long_call.option.delta()).abs()) @@ -1458,6 +1409,7 @@ mod tests_iron_condor_strategies { #[cfg(test)] mod tests_iron_condor_optimizable { use super::*; + use crate::chains::chain::OptionData; use crate::model::types::ExpirationDate; use crate::pos; use crate::spos; @@ -1485,7 +1437,7 @@ mod tests_iron_condor_optimizable { } fn create_test_chain() -> OptionChain { - let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-12-31".to_string()); + let mut chain = OptionChain::new("TEST", pos!(100.0), "2024-12-31".to_string(), None, None); // Add options at various strikes for strike in [85.0, 90.0, 95.0, 100.0, 105.0, 110.0, 115.0] { @@ -1603,22 +1555,6 @@ mod tests_iron_condor_optimizable { .is_valid_short_option(&option, &FindOptimalSide::Range(pos!(100.0), pos!(110.0)))); } - #[test] - fn test_are_valid_prices() { - let condor = create_test_condor(); - let chain = create_test_chain(); - let options: Vec<&OptionData> = chain.options.iter().collect(); - - let legs = StrategyLegs::FourLegs { - first: options[1], // 90.0 strike for long put - second: options[2], // 95.0 strike for short put - third: options[4], // 105.0 strike for short call - fourth: options[5], // 110.0 strike for long call - }; - - assert!(condor.are_valid_prices(&legs)); - } - #[test] fn test_create_strategy() { let condor = create_test_condor(); diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs index 0bf4b992..7763df4b 100644 --- a/src/strategies/mod.rs +++ b/src/strategies/mod.rs @@ -41,6 +41,7 @@ //! Example usage of the Bull Call Spread strategy: //! //! ```rust +//! use tracing::info; //! use optionstratlib::model::types::{ExpirationDate, PZERO}; //! use optionstratlib::strategies::bull_call_spread::BullCallSpread; //! use optionstratlib::model::types::PositiveF64; @@ -67,7 +68,7 @@ //! //! let profit = spread.max_profit().unwrap_or(PZERO); //! let loss = spread.max_loss().unwrap_or(PZERO); -//! println!("Max Profit: {}, Max Loss: {}", profit, loss); +//! info!("Max Profit: {}, Max Loss: {}", profit, loss); //! ``` //! //! Refer to the documentation of each sub-module for more details on the specific @@ -145,6 +146,7 @@ //! //! Example usage of the Iron Condor strategy: //! //! ```rust +//! use tracing::info; //! use optionstratlib::model::types::{ExpirationDate, PZERO}; //! use optionstratlib::strategies::iron_condor::IronCondor; //! use optionstratlib::model::types::PositiveF64; @@ -173,7 +175,7 @@ //! //! let max_profit = condor.max_profit().unwrap_or(PZERO); //! let max_loss = condor.max_loss().unwrap_or(PZERO); -//! println!("Max Profit: {}, Max Loss: {}", max_profit, max_loss); +//! info!("Max Profit: {}, Max Loss: {}", max_profit, max_loss); //! ``` //! //! Refer to the documentation of each sub-module for more details on the specific diff --git a/src/strategies/poor_mans_covered_call.rs b/src/strategies/poor_mans_covered_call.rs index caadfca8..29aa65ae 100644 --- a/src/strategies/poor_mans_covered_call.rs +++ b/src/strategies/poor_mans_covered_call.rs @@ -440,7 +440,7 @@ impl Graph for PoorMansCoveredCall { ), format!( "Short Call Expiry: {}", - self.short_call.option.expiration_date + self.short_call.option.expiration_date.get_date_string() ), ] .iter() @@ -840,7 +840,7 @@ mod tests_pmcc_optimization { use crate::spos; fn create_test_option_chain() -> OptionChain { - let mut chain = OptionChain::new("AAPL", pos!(150.0), "2024-01-01".to_string()); + let mut chain = OptionChain::new("AAPL", pos!(150.0), "2024-01-01".to_string(), None, None); // Add options at various strikes for strike in [140.0, 145.0, 150.0, 155.0, 160.0].iter() { diff --git a/src/strategies/probabilities/mod.rs b/src/strategies/probabilities/mod.rs index 32d930fd..10b0d3cc 100644 --- a/src/strategies/probabilities/mod.rs +++ b/src/strategies/probabilities/mod.rs @@ -55,6 +55,7 @@ //! ### Basic Strategy Analysis //! //! ```rust +//! use tracing::info; //! use optionstratlib::model::types::{ExpirationDate, OptionStyle, OptionType, Side}; //! use optionstratlib::strategies::probabilities::{ProbabilityAnalysis, VolatilityAdjustment, PriceTrend, StrategyProbabilityAnalysis}; //! use optionstratlib::model::types::PositiveF64; @@ -80,7 +81,7 @@ //! ); //! let analysis = strategy.analyze_probabilities(None, None); //! -//! println!("Analysis: {:?}", analysis); +//! info!("Analysis: {:?}", analysis); //! ``` //! //! ### Analysis with Volatility Adjustment @@ -154,6 +155,7 @@ //! ### Price Range Probability Analysis //! //! ```rust +//! use tracing::info; //! use optionstratlib::strategies::probabilities::calculate_price_probability; //! use optionstratlib::model::types::ExpirationDate; //! use optionstratlib::model::types::PositiveF64; @@ -168,7 +170,7 @@ //! ExpirationDate::Days(30.0), //! None // risk-free rate //! ).unwrap(); -//! println!("Probabilities: {}, {}, {}", prob_below, prob_in_range, prob_above); +//! info!("Probabilities: {}, {}, {}", prob_below, prob_in_range, prob_above); //! ``` //! //! ## Mathematical Models diff --git a/src/strategies/straddle.rs b/src/strategies/straddle.rs index e00cf248..35f0300f 100644 --- a/src/strategies/straddle.rs +++ b/src/strategies/straddle.rs @@ -11,7 +11,8 @@ Key characteristics: */ use super::base::{Optimizable, Positionable, Strategies, StrategyType, Validable}; -use crate::chains::chain::{OptionChain, OptionData}; +use crate::chains::chain::OptionChain; +use crate::chains::utils::OptionDataGroup; use crate::chains::StrategyLegs; use crate::constants::{DARK_BLUE, DARK_GREEN, ZERO}; use crate::greeks::equations::{Greek, Greeks}; @@ -34,7 +35,7 @@ use chrono::Utc; use plotters::prelude::full_palette::ORANGE; use plotters::prelude::{ShapeStyle, RED}; use std::f64; -use tracing::{debug, error, trace}; +use tracing::{info, trace}; /// A Short Straddle is an options trading strategy that involves simultaneously selling /// a put and a call option with the same strike price and expiration date. This neutral @@ -243,127 +244,70 @@ impl Validable for ShortStraddle { impl Optimizable for ShortStraddle { type Strategy = ShortStraddle; + fn filter_combinations<'a>( + &'a self, + option_chain: &'a OptionChain, + side: FindOptimalSide, + ) -> impl Iterator> { + let underlying_price = self.get_underlying_price(); + let strategy = self.clone(); + option_chain + .get_single_iter() + // Filter out invalid combinations based on FindOptimalSide + .filter(move |both| both.is_valid_optimal_side(underlying_price, &side)) + .filter(|both| { + both.call_ask.unwrap_or(PZERO) > PZERO && both.call_bid.unwrap_or(PZERO) > PZERO + }) + // Filter out options that don't meet strategy constraints + .filter(move |both| { + let legs = StrategyLegs::TwoLegs { + first: both, + second: both, + }; + let strategy = strategy.create_strategy(option_chain, &legs); + strategy.validate() && strategy.max_profit().is_ok() && strategy.max_loss().is_ok() + }) + // Map to OptionDataGroup + .map(OptionDataGroup::One) + } + fn find_optimal( &mut self, option_chain: &OptionChain, side: FindOptimalSide, criteria: OptimizationCriteria, ) { - let options: Vec<&OptionData> = option_chain.options.iter().collect(); let mut best_value = f64::NEG_INFINITY; - - for call_index in 0..options.len() { - let call_option = &options[call_index]; - - for put_option in &options[..call_index] { - if call_option.strike_price <= put_option.strike_price { - error!( - "Invalid strike prices CALL: {:#?} PUT: {:#?}", - call_option.strike_price, put_option.strike_price - ); - continue; - } - - if !self.is_valid_short_option(put_option, &side) - || !self.is_valid_short_option(call_option, &side) - { - continue; - } - - let legs = StrategyLegs::TwoLegs { - first: call_option, - second: put_option, - }; - - if !self.are_valid_prices(&legs) { - error!( - "Invalid Bid prices Put({}): {:?} Call({}): {:?} ", - put_option.strike_price, - put_option.put_bid.unwrap_or(PZERO), - call_option.strike_price, - call_option.call_bid.unwrap_or(PZERO) - ); - continue; - } - - debug!("Creating Strategy"); - let strategy: ShortStraddle = self.create_strategy(option_chain, &legs); - - if !strategy.validate() { - continue; - } - - let current_value = match criteria { - OptimizationCriteria::Ratio => strategy.profit_ratio(), - OptimizationCriteria::Area => strategy.profit_area(), - }; - - if current_value > best_value { - best_value = current_value; - *self = strategy.clone(); - } - } - } - debug!("Best Value: {}", best_value); - } - - fn is_valid_short_option(&self, option: &OptionData, side: &FindOptimalSide) -> bool { - let underlying_price = match ( - self.short_put.option.underlying_price, - self.short_call.option.underlying_price, - ) { - (PZERO, PZERO) => PZERO, - (PZERO, call) => call, - (put, _) => put, - }; - if underlying_price == PZERO { - error!("Invalid underlying_price option"); - return false; - } - - match side { - FindOptimalSide::Upper => { - let valid = option.strike_price >= underlying_price; - if !valid { - debug!( - "Option is out of range: {} <= {}", - option.strike_price, underlying_price - ); - } - valid - } - FindOptimalSide::Lower => { - let valid = option.strike_price <= underlying_price; - if !valid { - debug!( - "Option is out of range: {} >= {}", - option.strike_price, underlying_price - ); - } - valid - } - FindOptimalSide::All => true, - FindOptimalSide::Range(start, end) => { - let valid = option.strike_price >= *start && option.strike_price <= *end; - if !valid { - debug!( - "Option is out of range: {} >= {} && {} <= {}", - option.strike_price, *start, option.strike_price, *end - ); - } - valid + let strategy_clone = self.clone(); + let options_iter = strategy_clone.filter_combinations(option_chain, side); + + for option_data_group in options_iter { + // Unpack the OptionDataGroup into individual options + let both = match option_data_group { + OptionDataGroup::One(first) => first, + _ => panic!("Invalid OptionDataGroup"), + }; + + let legs = StrategyLegs::TwoLegs { + first: both, + second: both, + }; + let strategy = self.create_strategy(option_chain, &legs); + // Calculate the current value based on the optimization criteria + let current_value = match criteria { + OptimizationCriteria::Ratio => strategy.profit_ratio(), + OptimizationCriteria::Area => strategy.profit_area(), + }; + + if current_value > best_value { + // Update the best value and replace the current strategy + info!("Found better value: {}", current_value); + best_value = current_value; + *self = strategy.clone(); } } } - fn are_valid_prices(&self, legs: &StrategyLegs) -> bool { - let (call, put) = match legs { - StrategyLegs::TwoLegs { first, second } => (first, second), - _ => panic!("Invalid number of legs for this strategy"), - }; - call.call_bid.unwrap() > PZERO && put.put_bid.unwrap() > PZERO - } - fn create_strategy(&self, chain: &OptionChain, legs: &StrategyLegs) -> Self::Strategy { let (call, put) = match legs { StrategyLegs::TwoLegs { first, second } => (first, second), @@ -833,129 +777,71 @@ impl Validable for LongStraddle { impl Optimizable for LongStraddle { type Strategy = LongStraddle; + + fn filter_combinations<'a>( + &'a self, + option_chain: &'a OptionChain, + side: FindOptimalSide, + ) -> impl Iterator> { + let underlying_price = self.get_underlying_price(); + let strategy = self.clone(); + option_chain + .get_single_iter() + // Filter out invalid combinations based on FindOptimalSide + .filter(move |both| both.is_valid_optimal_side(underlying_price, &side)) + .filter(|both| { + both.call_ask.unwrap_or(PZERO) > PZERO && both.call_bid.unwrap_or(PZERO) > PZERO + }) + // Filter out options that don't meet strategy constraints + .filter(move |both| { + let legs = StrategyLegs::TwoLegs { + first: both, + second: both, + }; + let strategy = strategy.create_strategy(option_chain, &legs); + strategy.validate() && strategy.max_profit().is_ok() && strategy.max_loss().is_ok() + }) + // Map to OptionDataGroup + .map(OptionDataGroup::One) + } + fn find_optimal( &mut self, option_chain: &OptionChain, side: FindOptimalSide, criteria: OptimizationCriteria, ) { - let options: Vec<&OptionData> = option_chain.options.iter().collect(); let mut best_value = f64::NEG_INFINITY; - - for call_index in 0..options.len() { - let call_option = &options[call_index]; - - for put_option in &options[..call_index] { - trace!( - "Call: {:#?} Put: {:#?}", - call_option.strike_price, - put_option.strike_price - ); - if call_option.strike_price <= put_option.strike_price { - error!( - "Invalid strike prices Put: {:#?} Call: {:#?} ", - put_option.strike_price, call_option.strike_price - ); - continue; - } - - if !self.is_valid_long_option(put_option, &side) - || !self.is_valid_long_option(call_option, &side) - { - error!("Invalid option"); - continue; - } - - let legs = StrategyLegs::TwoLegs { - first: call_option, - second: put_option, - }; - - if !self.are_valid_prices(&legs) { - error!( - "Invalid Ask prices Put: {:#?} Call: {:#?} ", - put_option.put_ask, call_option.call_ask - ); - continue; - } - - let strategy: LongStraddle = self.create_strategy(option_chain, &legs); - - if !strategy.validate() { - error!("Invalid strategy"); - continue; - } - - let current_value = match criteria { - OptimizationCriteria::Ratio => strategy.profit_ratio(), - OptimizationCriteria::Area => strategy.profit_area(), - }; - - if current_value > best_value { - best_value = current_value; - *self = strategy.clone(); - } - } - } - } - - fn is_valid_long_option(&self, option: &OptionData, side: &FindOptimalSide) -> bool { - let underlying_price = match ( - self.long_put.option.underlying_price, - self.long_call.option.underlying_price, - ) { - (PZERO, PZERO) => PZERO, - (PZERO, call) => call, - (put, _) => put, - }; - if underlying_price == PZERO { - error!("Invalid underlying_price option"); - return false; - } - - match side { - FindOptimalSide::Upper => { - let valid = option.strike_price >= underlying_price; - if !valid { - debug!( - "Option is out of range: {} <= {}", - option.strike_price, underlying_price - ); - } - valid - } - FindOptimalSide::Lower => { - let valid = option.strike_price <= underlying_price; - if !valid { - debug!( - "Option is out of range: {} >= {}", - option.strike_price, underlying_price - ); - } - valid - } - FindOptimalSide::All => true, - FindOptimalSide::Range(start, end) => { - let valid = option.strike_price >= *start && option.strike_price <= *end; - if !valid { - debug!( - "Option is out of range: {} >= {} && {} <= {}", - option.strike_price, *start, option.strike_price, *end - ); - } - valid + let strategy_clone = self.clone(); + let options_iter = strategy_clone.filter_combinations(option_chain, side); + + for option_data_group in options_iter { + // Unpack the OptionDataGroup into individual options + let both = match option_data_group { + OptionDataGroup::One(first) => first, + _ => panic!("Invalid OptionDataGroup"), + }; + + let legs = StrategyLegs::TwoLegs { + first: both, + second: both, + }; + let strategy = self.create_strategy(option_chain, &legs); + // Calculate the current value based on the optimization criteria + let current_value = match criteria { + OptimizationCriteria::Ratio => strategy.profit_ratio(), + OptimizationCriteria::Area => strategy.profit_area(), + }; + + if current_value > best_value { + // Update the best value and replace the current strategy + info!("Found better value: {}", current_value); + best_value = current_value; + *self = strategy.clone(); } } } - fn are_valid_prices(&self, legs: &StrategyLegs) -> bool { - let (call, put) = match legs { - StrategyLegs::TwoLegs { first, second } => (first, second), - _ => panic!("Invalid number of legs for this strategy"), - }; - call.call_ask.unwrap() > PZERO && put.put_ask.unwrap() > PZERO - } - fn create_strategy(&self, chain: &OptionChain, legs: &StrategyLegs) -> Self::Strategy { let (call, put) = match legs { StrategyLegs::TwoLegs { first, second } => (first, second), @@ -1470,29 +1356,6 @@ mod tests_short_straddle { .is_valid_short_option(option_data, &FindOptimalSide::Range(min_strike, max_strike))); } - #[test] - fn test_are_valid_prices() { - let strategy = setup(); - let option_chain = create_test_option_chain(); - let call_option = option_chain.options.last().unwrap(); - let put_option = option_chain.options.first().unwrap(); - - let legs = StrategyLegs::TwoLegs { - first: call_option, - second: put_option, - }; - assert!(strategy.are_valid_prices(&legs)); - - let mut invalid_call = call_option.clone(); - invalid_call.call_bid = Some(pos!(0.0)); - - let legs = StrategyLegs::TwoLegs { - first: &invalid_call, - second: put_option, - }; - assert!(!strategy.are_valid_prices(&legs)); - } - #[test] fn test_create_strategy() { let strategy = setup(); diff --git a/src/strategies/strangle.rs b/src/strategies/strangle.rs index 33d2b003..b3fea977 100644 --- a/src/strategies/strangle.rs +++ b/src/strategies/strangle.rs @@ -10,7 +10,8 @@ Key characteristics: - Requires a larger price move to become profitable */ use super::base::{Optimizable, Positionable, Strategies, StrategyType, Validable}; -use crate::chains::chain::{OptionChain, OptionData}; +use crate::chains::chain::OptionChain; +use crate::chains::utils::OptionDataGroup; use crate::chains::StrategyLegs; use crate::constants::{DARK_BLUE, DARK_GREEN, ZERO}; use crate::greeks::equations::{Greek, Greeks}; @@ -33,7 +34,7 @@ use chrono::Utc; use plotters::prelude::full_palette::ORANGE; use plotters::prelude::{ShapeStyle, RED}; use std::f64; -use tracing::{debug, error, info, trace}; +use tracing::{debug, info, trace}; const SHORT_STRANGLE_DESCRIPTION: &str = "A short strangle involves selling an out-of-the-money call and an \ @@ -246,129 +247,78 @@ impl Validable for ShortStrangle { impl Optimizable for ShortStrangle { type Strategy = ShortStrangle; + fn filter_combinations<'a>( + &'a self, + option_chain: &'a OptionChain, + side: FindOptimalSide, + ) -> impl Iterator> { + let underlying_price = self.get_underlying_price(); + let strategy = self.clone(); + option_chain + .get_double_iter() + // Filter out invalid combinations based on FindOptimalSide + .filter(move |(short_put, short_call)| { + short_put.is_valid_optimal_side(underlying_price, &side) + && short_call.is_valid_optimal_side(underlying_price, &side) + }) + .filter(move |(short_put, short_call)| short_put.strike_price < short_call.strike_price) + // Filter out options with invalid bid/ask prices + .filter(|(short_put, short_call)| { + short_put.call_ask.unwrap_or(PZERO) > PZERO + && short_call.call_bid.unwrap_or(PZERO) > PZERO + }) + // Filter out options that don't meet strategy constraints + .filter(move |(short_put, short_call)| { + let legs = StrategyLegs::TwoLegs { + first: short_put, + second: short_call, + }; + let strategy = strategy.create_strategy(option_chain, &legs); + strategy.validate() && strategy.max_profit().is_ok() && strategy.max_loss().is_ok() + }) + // Map to OptionDataGroup + .map(move |(short_put, short_call)| OptionDataGroup::Two(short_put, short_call)) + } + fn find_optimal( &mut self, option_chain: &OptionChain, side: FindOptimalSide, criteria: OptimizationCriteria, ) { - let options: Vec<&OptionData> = option_chain.options.iter().collect(); let mut best_value = f64::NEG_INFINITY; - - for call_index in 0..options.len() { - let call_option = &options[call_index]; - - for put_option in &options[..call_index] { - if call_option.strike_price <= put_option.strike_price { - error!( - "Invalid strike prices CALL: {:#?} PUT: {:#?}", - call_option.strike_price, put_option.strike_price - ); - continue; - } - - if !self.is_valid_short_option(put_option, &side) - || !self.is_valid_short_option(call_option, &side) - { - continue; - } - - let legs = StrategyLegs::TwoLegs { - first: call_option, - second: put_option, - }; - - if !self.are_valid_prices(&legs) { - error!( - "Invalid Bid prices Put({}): {:?} Call({}): {:?} ", - put_option.strike_price, - put_option.put_bid.unwrap_or(PZERO), - call_option.strike_price, - call_option.call_bid.unwrap_or(PZERO) - ); - continue; - } - - debug!("Creating Strategy"); - let strategy: ShortStrangle = self.create_strategy(option_chain, &legs); - - if !strategy.validate() { - continue; - } - - let current_value = match criteria { - OptimizationCriteria::Ratio => strategy.profit_ratio(), - OptimizationCriteria::Area => strategy.profit_area(), - }; - - if current_value > best_value { - best_value = current_value; - *self = strategy.clone(); - } + let strategy_clone = self.clone(); + let options_iter = strategy_clone.filter_combinations(option_chain, side); + + for option_data_group in options_iter { + // Unpack the OptionDataGroup into individual options + let (short_put, short_call) = match option_data_group { + OptionDataGroup::Two(first, second) => (first, second), + _ => panic!("Invalid OptionDataGroup"), + }; + + let legs = StrategyLegs::TwoLegs { + first: short_put, + second: short_call, + }; + let strategy = self.create_strategy(option_chain, &legs); + // Calculate the current value based on the optimization criteria + let current_value = match criteria { + OptimizationCriteria::Ratio => strategy.profit_ratio(), + OptimizationCriteria::Area => strategy.profit_area(), + }; + + if current_value > best_value { + // Update the best value and replace the current strategy + info!("Found better value: {}", current_value); + best_value = current_value; + *self = strategy.clone(); } } - debug!("Best Value: {}", best_value); - } - - fn is_valid_short_option(&self, option: &OptionData, side: &FindOptimalSide) -> bool { - let underlying_price = match ( - self.short_put.option.underlying_price, - self.short_call.option.underlying_price, - ) { - (PZERO, PZERO) => PZERO, - (PZERO, call) => call, - (put, _) => put, - }; - if underlying_price == PZERO { - error!("Invalid underlying_price option"); - return false; - } - - match side { - FindOptimalSide::Upper => { - let valid = option.strike_price >= underlying_price; - if !valid { - debug!( - "Option is out of range: {} <= {}", - option.strike_price, underlying_price - ); - } - valid - } - FindOptimalSide::Lower => { - let valid = option.strike_price <= underlying_price; - if !valid { - debug!( - "Option is out of range: {} >= {}", - option.strike_price, underlying_price - ); - } - valid - } - FindOptimalSide::All => true, - FindOptimalSide::Range(start, end) => { - let valid = option.strike_price >= *start && option.strike_price <= *end; - if !valid { - debug!( - "Option is out of range: {} >= {} && {} <= {}", - option.strike_price, *start, option.strike_price, *end - ); - } - valid - } - } - } - - fn are_valid_prices(&self, legs: &StrategyLegs) -> bool { - let (call, put) = match legs { - StrategyLegs::TwoLegs { first, second } => (first, second), - _ => panic!("Invalid number of legs for this strategy"), - }; - call.call_bid.unwrap() > PZERO && put.put_bid.unwrap() > PZERO } fn create_strategy(&self, chain: &OptionChain, legs: &StrategyLegs) -> Self::Strategy { - let (call, put) = match legs { + let (put, call) = match legs { StrategyLegs::TwoLegs { first, second } => (first, second), _ => panic!("Invalid number of legs for this strategy"), }; @@ -448,66 +398,78 @@ impl Graph for ShortStrangle { let mut points: Vec> = Vec::new(); let max_profit = self.max_profit().unwrap_or(PZERO); + let coordinates: (f64, f64) = (-3.0, 30.0); + let font_size = 24; + points.push(ChartPoint { coordinates: (self.break_even_points[0].value(), 0.0), label: format!("Low Break Even\n\n{}", self.break_even_points[0]), - label_offset: LabelOffsetType::Relative(0.0, -10.0), + label_offset: LabelOffsetType::Relative(coordinates.0, -coordinates.1), point_color: DARK_BLUE, label_color: DARK_BLUE, point_size: 5, - font_size: 18, + font_size, }); points.push(ChartPoint { coordinates: (self.break_even_points[1].value(), 0.0), label: format!("High Break Even\n\n{}", self.break_even_points[1]), - label_offset: LabelOffsetType::Relative(-230.0, -10.0), + label_offset: LabelOffsetType::Relative(coordinates.0 * 130.0, -coordinates.1), point_color: DARK_BLUE, label_color: DARK_BLUE, point_size: 5, - font_size: 18, + font_size, }); - let coordiantes: (f64, f64) = ( - self.short_put.option.strike_price.value() / 250.0, - max_profit.value() / 15.0, - ); points.push(ChartPoint { coordinates: ( self.short_call.option.strike_price.value(), max_profit.value(), ), label: format!( - "Max Profit {:.2} at {:.0}", + "Max Profit ${:.2} at {:.0}", max_profit, self.short_call.option.strike_price ), - label_offset: LabelOffsetType::Relative(coordiantes.0, coordiantes.1), + label_offset: LabelOffsetType::Relative(coordinates.0, coordinates.1), point_color: DARK_GREEN, label_color: DARK_GREEN, point_size: 5, - font_size: 18, + font_size, }); - let coordiantes: (f64, f64) = ( - -self.short_put.option.strike_price.value() / 30.0, - max_profit.value() / 15.0, - ); points.push(ChartPoint { coordinates: ( self.short_put.option.strike_price.value(), max_profit.value(), ), label: format!( - "Max Profit {:.2} at {:.0}", + "Max Profit ${:.2} at {:.0}", max_profit, self.short_put.option.strike_price ), - label_offset: LabelOffsetType::Relative(coordiantes.0, coordiantes.1), + label_offset: LabelOffsetType::Relative(coordinates.0 * 130.0, coordinates.1), point_color: DARK_GREEN, label_color: DARK_GREEN, point_size: 5, - font_size: 18, + font_size, + }); + + points.push(ChartPoint { + coordinates: ( + self.short_put.option.underlying_price.value(), + self.calculate_profit_at(self.short_put.option.underlying_price), + ), + label: format!( + "${:.2}", + self.calculate_profit_at(self.short_put.option.underlying_price), + ), + label_offset: LabelOffsetType::Relative(-coordinates.0 * 10.0, -coordinates.1), + point_color: DARK_GREEN, + label_color: DARK_GREEN, + point_size: 5, + font_size, }); - points.push(self.get_point_at_price(self.short_put.option.underlying_price)); + + // points.push(self.get_point_at_price(self.short_put.option.underlying_price)); points } @@ -860,136 +822,84 @@ impl Validable for LongStrangle { self.long_call.validate() && self.long_put.validate() && self.long_call.option.strike_price > self.long_put.option.strike_price - && self.long_put.option.strike_price > PZERO } } impl Optimizable for LongStrangle { type Strategy = LongStrangle; + + fn filter_combinations<'a>( + &'a self, + option_chain: &'a OptionChain, + side: FindOptimalSide, + ) -> impl Iterator> { + let underlying_price = self.get_underlying_price(); + let strategy = self.clone(); + option_chain + .get_double_iter() + // Filter out invalid combinations based on FindOptimalSide + .filter(move |(long_put, long_call)| { + long_put.is_valid_optimal_side(underlying_price, &side) + && long_call.is_valid_optimal_side(underlying_price, &side) + }) + .filter(move |(long_put, long_call)| long_put.strike_price < long_call.strike_price) + // Filter out options with invalid bid/ask prices + .filter(|(long_put, long_call)| { + long_put.call_ask.unwrap_or(PZERO) > PZERO + && long_call.call_bid.unwrap_or(PZERO) > PZERO + }) + // Filter out options that don't meet strategy constraints + .filter(move |(long_put, long_call)| { + let legs = StrategyLegs::TwoLegs { + first: long_put, + second: long_call, + }; + let strategy = strategy.create_strategy(option_chain, &legs); + strategy.validate() && strategy.max_profit().is_ok() && strategy.max_loss().is_ok() + }) + // Map to OptionDataGroup + .map(move |(long_put, long_call)| OptionDataGroup::Two(long_put, long_call)) + } + fn find_optimal( &mut self, option_chain: &OptionChain, side: FindOptimalSide, criteria: OptimizationCriteria, ) { - let options: Vec<&OptionData> = option_chain.options.iter().collect(); let mut best_value = f64::NEG_INFINITY; - - for call_index in 0..options.len() { - let call_option = &options[call_index]; - - for put_option in &options[..call_index] { - trace!( - "Call: {:#?} Put: {:#?}", - call_option.strike_price, - put_option.strike_price - ); - if call_option.strike_price <= put_option.strike_price { - error!( - "Invalid strike prices Put: {:#?} Call: {:#?} ", - put_option.strike_price, call_option.strike_price - ); - continue; - } - - if !self.is_valid_long_option(put_option, &side) - || !self.is_valid_long_option(call_option, &side) - { - error!("Invalid option"); - continue; - } - let legs = StrategyLegs::TwoLegs { - first: call_option, - second: put_option, - }; - - if !self.are_valid_prices(&legs) { - error!( - "Invalid Ask prices Put: {:#?} Call: {:#?} ", - put_option.put_ask, call_option.call_ask - ); - continue; - } - - let strategy: LongStrangle = self.create_strategy(option_chain, &legs); - - if !strategy.validate() { - error!("Invalid strategy"); - continue; - } - - let current_value = match criteria { - OptimizationCriteria::Ratio => strategy.profit_ratio(), - OptimizationCriteria::Area => strategy.profit_area(), - }; - - if current_value > best_value { - best_value = current_value; - *self = strategy.clone(); - } - } - } - } - - fn is_valid_long_option(&self, option: &OptionData, side: &FindOptimalSide) -> bool { - let underlying_price = match ( - self.long_put.option.underlying_price, - self.long_call.option.underlying_price, - ) { - (PZERO, PZERO) => PZERO, - (PZERO, call) => call, - (put, _) => put, - }; - if underlying_price == PZERO { - error!("Invalid underlying_price option"); - return false; - } - - match side { - FindOptimalSide::Upper => { - let valid = option.strike_price >= underlying_price; - if !valid { - debug!( - "Option is out of range: {} <= {}", - option.strike_price, underlying_price - ); - } - valid - } - FindOptimalSide::Lower => { - let valid = option.strike_price <= underlying_price; - if !valid { - debug!( - "Option is out of range: {} >= {}", - option.strike_price, underlying_price - ); - } - valid - } - FindOptimalSide::All => true, - FindOptimalSide::Range(start, end) => { - let valid = option.strike_price >= *start && option.strike_price <= *end; - if !valid { - debug!( - "Option is out of range: {} >= {} && {} <= {}", - option.strike_price, *start, option.strike_price, *end - ); - } - valid + let strategy_clone = self.clone(); + let options_iter = strategy_clone.filter_combinations(option_chain, side); + + for option_data_group in options_iter { + // Unpack the OptionDataGroup into individual options + let (long_put, long_call) = match option_data_group { + OptionDataGroup::Two(first, second) => (first, second), + _ => panic!("Invalid OptionDataGroup"), + }; + + let legs = StrategyLegs::TwoLegs { + first: long_put, + second: long_call, + }; + let strategy = self.create_strategy(option_chain, &legs); + // Calculate the current value based on the optimization criteria + let current_value = match criteria { + OptimizationCriteria::Ratio => strategy.profit_ratio(), + OptimizationCriteria::Area => strategy.profit_area(), + }; + + if current_value > best_value { + // Update the best value and replace the current strategy + info!("Found better value: {}", current_value); + best_value = current_value; + *self = strategy.clone(); } } } - fn are_valid_prices(&self, legs: &StrategyLegs) -> bool { - let (call, put) = match legs { - StrategyLegs::TwoLegs { first, second } => (first, second), - _ => panic!("Invalid number of legs for this strategy"), - }; - call.call_ask.unwrap() > PZERO && put.put_ask.unwrap() > PZERO - } - fn create_strategy(&self, chain: &OptionChain, legs: &StrategyLegs) -> Self::Strategy { - let (call, put) = match legs { + let (put, call) = match legs { StrategyLegs::TwoLegs { first, second } => (first, second), _ => panic!("Invalid number of legs for this strategy"), }; @@ -1478,29 +1388,6 @@ is expected and the underlying asset's price is anticipated to remain stable." .is_valid_short_option(option_data, &FindOptimalSide::Range(min_strike, max_strike))); } - #[test] - fn test_are_valid_prices() { - let strategy = setup(); - let option_chain = create_test_option_chain(); - let call_option = option_chain.options.last().unwrap(); - let put_option = option_chain.options.first().unwrap(); - - let legs = StrategyLegs::TwoLegs { - first: call_option, - second: put_option, - }; - assert!(strategy.are_valid_prices(&legs)); - - let mut invalid_call = call_option.clone(); - invalid_call.call_bid = Some(pos!(0.0)); - - let legs = StrategyLegs::TwoLegs { - first: &invalid_call, - second: put_option, - }; - assert!(!strategy.are_valid_prices(&legs)); - } - #[test] fn test_create_strategy() { let strategy = setup(); @@ -1514,7 +1401,7 @@ is expected and the underlying asset's price is anticipated to remain stable." }; let new_strategy = strategy.create_strategy(&chain, &legs); - assert!(!new_strategy.validate()); + assert!(new_strategy.validate()); let call_option = chain.options.last().unwrap(); let put_option = chain.options.first().unwrap(); @@ -1525,7 +1412,7 @@ is expected and the underlying asset's price is anticipated to remain stable." }; let new_strategy = strategy.create_strategy(&chain, &legs); - assert!(new_strategy.validate()); + assert!(!new_strategy.validate()); } #[test] @@ -1860,7 +1747,7 @@ mod tests_long_strangle { second: put_option, }; let new_strategy = strategy.create_strategy(&chain, &legs); - assert!(!new_strategy.validate()); + assert!(new_strategy.validate()); let call_option = chain.options.last().unwrap(); let put_option = chain.options.first().unwrap(); @@ -1869,7 +1756,7 @@ mod tests_long_strangle { second: put_option, }; let new_strategy = strategy.create_strategy(&chain, &legs); - assert!(new_strategy.validate()); + assert!(!new_strategy.validate()); } #[test] diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 96e7b51b..845b167f 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -92,6 +92,7 @@ //! ### Example: Time Frame Usage //! //! ```rust +//! use tracing::info; //! use optionstratlib::utils::time::TimeFrame; //! //! let timeframes = vec![ @@ -102,7 +103,7 @@ //! ]; //! //! for tf in timeframes { -//! println!("Periods per year: {}", tf.periods_per_year()); +//! info!("Periods per year: {}", tf.periods_per_year()); //! } //! ``` //! diff --git a/src/utils/others.rs b/src/utils/others.rs index 86165170..703d238b 100644 --- a/src/utils/others.rs +++ b/src/utils/others.rs @@ -59,23 +59,58 @@ pub fn get_random_element(set: &BTreeSet) -> Option<&T> { /// # Errors: /// This function will return an error if the `positions` slice is empty. /// +// pub fn process_n_times_iter( +// positions: &[T], +// n: usize, +// mut process_combination: F, +// ) -> Result, String> +// where +// F: FnMut(&[&T]) -> Vec, +// T: Clone, +// { +// if positions.is_empty() { +// return Err("Vector empty".to_string()); +// } +// +// Ok(positions +// .iter() +// .combinations_with_replacement(n) +// .flat_map(|combination| process_combination(&combination)) +// .collect()) +// } + +use rayon::prelude::*; + pub fn process_n_times_iter( positions: &[T], n: usize, - mut process_combination: F, + process_combination: F, ) -> Result, String> where - F: FnMut(&[&T]) -> Vec, - T: Clone, + F: FnMut(&[&T]) -> Vec + Send + Sync, // Mantenemos FnMut + T: Clone + Send + Sync, + Y: Send, { if positions.is_empty() { return Err("Vector empty".to_string()); } - Ok(positions + // Generamos todas las combinaciones primero + let combinations: Vec<_> = positions .iter() .combinations_with_replacement(n) - .flat_map(|combination| process_combination(&combination)) + .collect(); + + // Para usar FnMut en paralelo, necesitamos envolverlo en Mutex + let process_combination = std::sync::Mutex::new(process_combination); + + // Procesamos en paralelo usando el Mutex + Ok(combinations + .par_iter() + .flat_map(|combination| { + let mut closure = process_combination.lock().unwrap(); + closure(combination) + }) .collect()) } @@ -224,6 +259,7 @@ mod tests_get_random_element { } } } + #[cfg(test)] mod tests_process_n_times_iter { use super::*; diff --git a/src/visualization/utils.rs b/src/visualization/utils.rs index 62d2da26..387124e1 100644 --- a/src/visualization/utils.rs +++ b/src/visualization/utils.rs @@ -48,6 +48,7 @@ macro_rules! configure_chart_and_draw_mesh { // Configure and draw the mesh grid $chart .configure_mesh() + .disable_mesh() // Disable the mesh grid .x_labels($x_labels) .y_labels($y_labels) .draw()?; diff --git a/tests/unit/mod.rs b/tests/unit/mod.rs index 25fba32b..16d25b6c 100644 --- a/tests/unit/mod.rs +++ b/tests/unit/mod.rs @@ -3,3 +3,5 @@ Email: jb@taunais.com Date: 1/8/24 ******************************************************************************/ + +mod strategies; diff --git a/tests/unit/strategies/delta/mod.rs b/tests/unit/strategies/delta/mod.rs new file mode 100644 index 00000000..264f571b --- /dev/null +++ b/tests/unit/strategies/delta/mod.rs @@ -0,0 +1,20 @@ +/****************************************************************************** + Author: Joaquín Béjar García + Email: jb@taunais.com + Date: 18/12/24 +******************************************************************************/ +mod strategy_bear_call_spread; +mod strategy_bear_put_spread; +mod strategy_bull_call_spread; +mod strategy_bull_put_spread; +mod strategy_call_butterfly; +mod strategy_custom; +mod strategy_iron_butterfly; +mod strategy_iron_condor; +mod strategy_long_butterfly_spread; +mod strategy_long_straddle; +mod strategy_long_strangle; +mod strategy_poor_mans_covered_call; +mod strategy_short_butterfly_spread; +mod strategy_short_straddle; +mod strategy_short_strangle; diff --git a/tests/unit/strategies/delta/strategy_bear_call_spread.rs b/tests/unit/strategies/delta/strategy_bear_call_spread.rs new file mode 100644 index 00000000..75837b6a --- /dev/null +++ b/tests/unit/strategies/delta/strategy_bear_call_spread.rs @@ -0,0 +1,73 @@ +use approx::assert_relative_eq; +use optionstratlib::greeks::equations::Greeks; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::model::types::{ExpirationDate, OptionStyle}; +use optionstratlib::pos; +use optionstratlib::strategies::bear_call_spread::BearCallSpread; +use optionstratlib::strategies::delta_neutral::DeltaAdjustment::BuyOptions; +use optionstratlib::strategies::delta_neutral::DeltaNeutrality; +use optionstratlib::utils::logger::setup_logger; +use std::error::Error; + +#[test] +fn test_bear_call_spread_integration() -> Result<(), Box> { + setup_logger(); + // Define inputs for the BearCallSpread strategy + let underlying_price = pos!(5781.88); + + let strategy = BearCallSpread::new( + "SP500".to_string(), + underlying_price, // underlying_price + pos!(5750.0), // long_strike_itm + pos!(5820.0), // short_strike + ExpirationDate::Days(2.0), + 0.18, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(2.0), // long quantity + 85.04, // premium_long + 29.85, // premium_short + 0.78, // open_fee_long + 0.78, // open_fee_long + 0.73, // close_fee_long + 0.73, // close_fee_short + ); + + let greeks = strategy.greeks(); + + assert_relative_eq!(greeks.delta, -0.7004, epsilon = 0.001); + assert_relative_eq!(greeks.gamma, 0.0186, epsilon = 0.001); + assert_relative_eq!(greeks.theta, -10685.1215, epsilon = 0.001); + assert_relative_eq!(greeks.vega, 848.6626, epsilon = 0.001); + assert_relative_eq!(greeks.rho, 62.0955, epsilon = 0.001); + assert_relative_eq!(greeks.rho_d, -62.8208, epsilon = 0.001); + + assert_relative_eq!( + strategy.calculate_net_delta().net_delta, + -0.7004, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[0], + 0.6412, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[1], + -1.3416, + epsilon = 0.001 + ); + assert!(!strategy.is_delta_neutral()); + assert_eq!(strategy.suggest_delta_adjustments().len(), 1); + + assert_eq!( + strategy.suggest_delta_adjustments()[0], + BuyOptions { + quantity: pos!(2.184538786861787), + strike: pos!(5820.0), + option_type: OptionStyle::Call + } + ); + + Ok(()) +} diff --git a/tests/unit/strategies/delta/strategy_bear_put_spread.rs b/tests/unit/strategies/delta/strategy_bear_put_spread.rs new file mode 100644 index 00000000..b14d129a --- /dev/null +++ b/tests/unit/strategies/delta/strategy_bear_put_spread.rs @@ -0,0 +1,74 @@ +use approx::assert_relative_eq; +use optionstratlib::greeks::equations::Greeks; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::model::types::{ExpirationDate, OptionStyle}; +use optionstratlib::pos; +use optionstratlib::strategies::bear_put_spread::BearPutSpread; +use optionstratlib::strategies::delta_neutral::DeltaAdjustment::SellOptions; +use optionstratlib::strategies::delta_neutral::DeltaNeutrality; +use optionstratlib::utils::logger::setup_logger; +use std::error::Error; + +#[test] +fn test_bear_put_spread_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the BearPutSpread strategy + let underlying_price = pos!(5781.88); + + let strategy = BearPutSpread::new( + "SP500".to_string(), + underlying_price, // underlying_price + pos!(5850.0), // long_strike + pos!(5720.0), // short_strike + ExpirationDate::Days(2.0), + 0.18, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(2.0), // long quantity + 85.04, // premium_long + 29.85, // premium_short + 0.78, // open_fee_long + 0.78, // open_fee_long + 0.73, // close_fee_long + 0.73, // close_fee_short + ); + + let greeks = strategy.greeks(); + + assert_relative_eq!(greeks.delta, -1.2018, epsilon = 0.001); + assert_relative_eq!(greeks.gamma, 0.0145, epsilon = 0.001); + assert_relative_eq!(greeks.theta, -7271.7206, epsilon = 0.001); + assert_relative_eq!(greeks.vega, 851.9072, epsilon = 0.001); + assert_relative_eq!(greeks.rho, -64.5820, epsilon = 0.001); + assert_relative_eq!(greeks.rho_d, 63.6651, epsilon = 0.001); + + assert_relative_eq!( + strategy.calculate_net_delta().net_delta, + -1.2018, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[0], + -1.6056, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[1], + 0.4038, + epsilon = 0.001 + ); + assert!(!strategy.is_delta_neutral()); + assert_eq!(strategy.suggest_delta_adjustments().len(), 1); + + assert_eq!( + strategy.suggest_delta_adjustments()[0], + SellOptions { + quantity: pos!(5.952144261472945), + strike: pos!(5720.0), + option_type: OptionStyle::Put + } + ); + + Ok(()) +} diff --git a/tests/unit/strategies/delta/strategy_bull_call_spread.rs b/tests/unit/strategies/delta/strategy_bull_call_spread.rs new file mode 100644 index 00000000..c0eef5e3 --- /dev/null +++ b/tests/unit/strategies/delta/strategy_bull_call_spread.rs @@ -0,0 +1,73 @@ +use approx::assert_relative_eq; +use optionstratlib::greeks::equations::Greeks; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::model::types::{ExpirationDate, OptionStyle}; +use optionstratlib::pos; +use optionstratlib::strategies::bull_call_spread::BullCallSpread; +use optionstratlib::strategies::delta_neutral::DeltaAdjustment::SellOptions; +use optionstratlib::strategies::delta_neutral::DeltaNeutrality; +use optionstratlib::utils::logger::setup_logger; +use std::error::Error; + +#[test] +fn test_bull_call_spread_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the BullCallSpread strategy + let underlying_price = pos!(5781.88); + + let strategy = BullCallSpread::new( + "SP500".to_string(), + underlying_price, // underlying_price + pos!(5750.0), // long_strike_itm + pos!(5820.0), // short_strike + ExpirationDate::Days(2.0), + 0.18, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(2.0), // long quantity + 85.04, // premium_long + 29.85, // premium_short + 0.78, // open_fee_long + 0.78, // open_fee_long + 0.73, // close_fee_long + 0.73, // close_fee_short + ); + let greeks = strategy.greeks(); + + assert_relative_eq!(greeks.delta, 0.7004, epsilon = 0.001); + assert_relative_eq!(greeks.gamma, 0.0186, epsilon = 0.001); + assert_relative_eq!(greeks.theta, -10685.1215, epsilon = 0.001); + assert_relative_eq!(greeks.vega, 848.6626, epsilon = 0.001); + assert_relative_eq!(greeks.rho, 62.0955, epsilon = 0.001); + assert_relative_eq!(greeks.rho_d, -62.8208, epsilon = 0.001); + + assert_relative_eq!( + strategy.calculate_net_delta().net_delta, + 0.7004, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[0], + 1.3416, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[1], + -0.6412, + epsilon = 0.001 + ); + assert!(!strategy.is_delta_neutral()); + assert_eq!(strategy.suggest_delta_adjustments().len(), 1); + + assert_eq!( + strategy.suggest_delta_adjustments()[0], + SellOptions { + quantity: pos!(2.184538786861787), + strike: pos!(5820.0), + option_type: OptionStyle::Call + } + ); + + Ok(()) +} diff --git a/tests/unit/strategies/delta/strategy_bull_put_spread.rs b/tests/unit/strategies/delta/strategy_bull_put_spread.rs new file mode 100644 index 00000000..cf341d6e --- /dev/null +++ b/tests/unit/strategies/delta/strategy_bull_put_spread.rs @@ -0,0 +1,74 @@ +use approx::assert_relative_eq; +use optionstratlib::greeks::equations::Greeks; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::model::types::{ExpirationDate, OptionStyle}; +use optionstratlib::pos; +use optionstratlib::strategies::bull_put_spread::BullPutSpread; +use optionstratlib::strategies::delta_neutral::DeltaAdjustment::BuyOptions; +use optionstratlib::strategies::delta_neutral::DeltaNeutrality; +use optionstratlib::utils::logger::setup_logger; +use std::error::Error; + +#[test] +fn test_bull_put_spread_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the BullPutSpread strategy + let underlying_price = pos!(5781.88); + + let strategy = BullPutSpread::new( + "SP500".to_string(), + underlying_price, // underlying_price + pos!(5750.0), // long_strike_itm + pos!(5920.0), // short_strike + ExpirationDate::Days(2.0), + 0.18, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(2.0), // long quantity + 15.04, // premium_long + 89.85, // premium_short + 0.78, // open_fee_long + 0.78, // open_fee_long + 0.73, // close_fee_long + 0.73, // close_fee_short + ); + + let greeks = strategy.greeks(); + + assert_relative_eq!(greeks.delta, 1.2605, epsilon = 0.001); + assert_relative_eq!(greeks.gamma, 0.0116, epsilon = 0.001); + assert_relative_eq!(greeks.theta, -5550.6484, epsilon = 0.001); + assert_relative_eq!(greeks.vega, 608.9101, epsilon = 0.001); + assert_relative_eq!(greeks.rho, -83.3461, epsilon = 0.001); + assert_relative_eq!(greeks.rho_d, 81.6525, epsilon = 0.001); + + assert_relative_eq!( + strategy.calculate_net_delta().net_delta, + 1.2605, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[0], + 1.9189, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[1], + -0.6583, + epsilon = 0.001 + ); + assert!(!strategy.is_delta_neutral()); + assert_eq!(strategy.suggest_delta_adjustments().len(), 1); + + assert_eq!( + strategy.suggest_delta_adjustments()[0], + BuyOptions { + quantity: pos!(3.82949671165404), + strike: pos!(5750.0), + option_type: OptionStyle::Put + } + ); + + Ok(()) +} diff --git a/tests/unit/strategies/delta/strategy_call_butterfly.rs b/tests/unit/strategies/delta/strategy_call_butterfly.rs new file mode 100644 index 00000000..6c017747 --- /dev/null +++ b/tests/unit/strategies/delta/strategy_call_butterfly.rs @@ -0,0 +1,78 @@ +use approx::assert_relative_eq; +use optionstratlib::greeks::equations::Greeks; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::model::types::{ExpirationDate, OptionStyle}; +use optionstratlib::pos; +use optionstratlib::strategies::call_butterfly::CallButterfly; +use optionstratlib::strategies::delta_neutral::DeltaAdjustment::SellOptions; +use optionstratlib::strategies::delta_neutral::DeltaNeutrality; +use optionstratlib::utils::logger::setup_logger; +use std::error::Error; + +#[test] +fn test_call_butterfly_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the CallButterfly strategy + let underlying_price = pos!(5781.88); + + let strategy = CallButterfly::new( + "SP500".to_string(), + underlying_price, // underlying_price + pos!(5750.0), // long_call_strike + pos!(5800.0), // short_call_low_strike + pos!(5850.0), // short_call_high_strike + ExpirationDate::Days(2.0), + 0.18, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(1.0), // long quantity + 85.04, // premium_long_itm + 53.04, // premium_long_otm + 28.85, // premium_short + 0.78, // premium_short + 0.78, // open_fee_long + 0.78, // close_fee_long + 0.73, // close_fee_short + 0.73, // close_fee_short + 0.73, // open_fee_short + ); + + let greeks = strategy.greeks(); + + assert_relative_eq!(greeks.delta, 0.0559, epsilon = 0.001); + assert_relative_eq!(greeks.gamma, 0.0133, epsilon = 0.001); + assert_relative_eq!(greeks.theta, -7606.7078, epsilon = 0.001); + assert_relative_eq!(greeks.vega, 550.2891, epsilon = 0.001); + assert_relative_eq!(greeks.rho, 40.2857, epsilon = 0.001); + assert_relative_eq!(greeks.rho_d, -40.7342, epsilon = 0.001); + + assert_relative_eq!( + strategy.calculate_net_delta().net_delta, + 0.0559, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[0], + -0.4177, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[1], + -0.1971, + epsilon = 0.001 + ); + assert!(!strategy.is_delta_neutral()); + assert_eq!(strategy.suggest_delta_adjustments().len(), 2); + + assert_eq!( + strategy.suggest_delta_adjustments()[0], + SellOptions { + quantity: pos!(0.1338190182607821), + strike: pos!(5800.0), + option_type: OptionStyle::Call + } + ); + + Ok(()) +} diff --git a/tests/unit/strategies/delta/strategy_custom.rs b/tests/unit/strategies/delta/strategy_custom.rs new file mode 100644 index 00000000..53fbf0d4 --- /dev/null +++ b/tests/unit/strategies/delta/strategy_custom.rs @@ -0,0 +1,129 @@ +use approx::assert_relative_eq; +use chrono::Utc; +use optionstratlib::greeks::equations::Greeks; +use optionstratlib::model::option::Options; +use optionstratlib::model::position::Position; +use optionstratlib::model::types::{ExpirationDate, OptionStyle, OptionType, PositiveF64, Side}; +use optionstratlib::pos; +use optionstratlib::strategies::custom::CustomStrategy; +use optionstratlib::utils::logger::setup_logger; +use std::error::Error; + +#[test] +fn test_custom_strategy_integration() -> Result<(), Box> { + setup_logger(); + + // Define common parameters + let underlying_price = pos!(2340.0); + let underlying_symbol = "GAS".to_string(); + let expiration = ExpirationDate::Days(6.0); + let implied_volatility = 0.73; + let risk_free_rate = 0.05; + let dividend_yield = 0.0; + + // Create positions + let positions = vec![ + Position::new( + Options::new( + OptionType::European, + Side::Short, + underlying_symbol.clone(), + pos!(2100.0), + expiration.clone(), + implied_volatility, + pos!(2.0), + underlying_price, + risk_free_rate, + OptionStyle::Call, + dividend_yield, + None, + ), + 192.0, + Utc::now(), + 7.51, + 7.51, + ), + Position::new( + Options::new( + OptionType::European, + Side::Short, + underlying_symbol.clone(), + pos!(2250.0), + expiration.clone(), + implied_volatility, + pos!(2.0), + underlying_price, + risk_free_rate, + OptionStyle::Call, + dividend_yield, + None, + ), + 88.0, + Utc::now(), + 6.68, + 6.68, + ), + Position::new( + Options::new( + OptionType::European, + Side::Short, + underlying_symbol.clone(), + pos!(2500.0), + expiration.clone(), + implied_volatility, + pos!(1.0), + underlying_price, + risk_free_rate, + OptionStyle::Put, + dividend_yield, + None, + ), + 55.0, + Utc::now(), + 6.68, + 6.68, + ), + Position::new( + Options::new( + OptionType::European, + Side::Short, + underlying_symbol.clone(), + pos!(2150.0), + expiration.clone(), + implied_volatility, + pos!(2.5), + underlying_price, + risk_free_rate, + OptionStyle::Put, + dividend_yield, + None, + ), + 21.0, + Utc::now(), + 4.91, + 4.91, + ), + ]; + + let strategy = CustomStrategy::new( + "Custom Strategy".to_string(), + underlying_symbol, + "Example of a custom strategy".to_string(), + underlying_price, + positions, + 0.01, + 100, + 0.1, + ); + + let greeks = strategy.greeks(); + + assert_relative_eq!(greeks.delta, -1.975, epsilon = 0.001); + assert_relative_eq!(greeks.gamma, 0.0093, epsilon = 0.001); + assert_relative_eq!(greeks.theta, -13818.8978, epsilon = 0.001); + assert_relative_eq!(greeks.vega, 1642.8158, epsilon = 0.001); + assert_relative_eq!(greeks.rho, 59.0889, epsilon = 0.001); + assert_relative_eq!(greeks.rho_d, -75.9988, epsilon = 0.001); + + Ok(()) +} diff --git a/tests/unit/strategies/delta/strategy_iron_butterfly.rs b/tests/unit/strategies/delta/strategy_iron_butterfly.rs new file mode 100644 index 00000000..77119bb3 --- /dev/null +++ b/tests/unit/strategies/delta/strategy_iron_butterfly.rs @@ -0,0 +1,75 @@ +use approx::assert_relative_eq; +use optionstratlib::greeks::equations::Greeks; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::model::types::{ExpirationDate, OptionStyle}; +use optionstratlib::pos; +use optionstratlib::strategies::delta_neutral::DeltaAdjustment::BuyOptions; +use optionstratlib::strategies::delta_neutral::DeltaNeutrality; +use optionstratlib::strategies::iron_butterfly::IronButterfly; +use optionstratlib::utils::logger::setup_logger; +use std::error::Error; + +#[test] +fn test_iron_butterfly_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the IronButterfly strategy + let underlying_price = pos!(2646.9); + + let strategy = IronButterfly::new( + "GOLD".to_string(), + underlying_price, // underlying_price + pos!(2725.0), // short_call_strike + pos!(2800.0), // long_call_strike + pos!(2500.0), // long_put_strike + ExpirationDate::Days(30.0), + 0.1548, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(2.0), // quantity + 38.8, // premium_short_call + 30.4, // premium_short_put + 23.3, // premium_long_call + 16.8, // premium_long_put + 0.96, // open_fee + 0.96, // close_fee + ); + + let greeks = strategy.greeks(); + + assert_relative_eq!(greeks.delta, 0.9103, epsilon = 0.001); + assert_relative_eq!(greeks.gamma, 0.0177, epsilon = 0.001); + assert_relative_eq!(greeks.theta, -1383.2828, epsilon = 0.001); + assert_relative_eq!(greeks.vega, 2478.3050, epsilon = 0.001); + assert_relative_eq!(greeks.rho, -179.6019, epsilon = 0.001); + assert_relative_eq!(greeks.rho_d, 159.7057, epsilon = 0.001); + + assert_relative_eq!( + strategy.calculate_net_delta().net_delta, + 0.9103, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[0], + 0.2492, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[1], + -0.1611, + epsilon = 0.001 + ); + assert!(!strategy.is_delta_neutral()); + assert_eq!(strategy.suggest_delta_adjustments().len(), 2); + + assert_eq!( + strategy.suggest_delta_adjustments()[0], + BuyOptions { + quantity: pos!(11.30151498857601), + strike: pos!(2500.0), + option_type: OptionStyle::Put + } + ); + + Ok(()) +} diff --git a/tests/unit/strategies/delta/strategy_iron_condor.rs b/tests/unit/strategies/delta/strategy_iron_condor.rs new file mode 100644 index 00000000..bb7b3101 --- /dev/null +++ b/tests/unit/strategies/delta/strategy_iron_condor.rs @@ -0,0 +1,76 @@ +use approx::assert_relative_eq; +use optionstratlib::greeks::equations::Greeks; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::model::types::{ExpirationDate, OptionStyle}; +use optionstratlib::pos; +use optionstratlib::strategies::delta_neutral::DeltaAdjustment::BuyOptions; +use optionstratlib::strategies::delta_neutral::DeltaNeutrality; +use optionstratlib::strategies::iron_condor::IronCondor; +use optionstratlib::utils::logger::setup_logger; +use std::error::Error; + +#[test] +fn test_iron_condor_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the IronCondor strategy + let underlying_price = pos!(2646.9); + + let strategy = IronCondor::new( + "GOLD".to_string(), + underlying_price, // underlying_price + pos!(2725.0), // short_call_strike + pos!(2560.0), // short_put_strike + pos!(2800.0), // long_call_strike + pos!(2500.0), // long_put_strike + ExpirationDate::Days(30.0), + 0.1548, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(2.0), // quantity + 38.8, // premium_short_call + 30.4, // premium_short_put + 23.3, // premium_long_call + 16.8, // premium_long_put + 0.96, // open_fee + 0.96, // close_fee + ); + + let greeks = strategy.greeks(); + + assert_relative_eq!(greeks.delta, -0.1148, epsilon = 0.001); + assert_relative_eq!(greeks.gamma, 0.0165, epsilon = 0.001); + assert_relative_eq!(greeks.theta, -1425.3530, epsilon = 0.001); + assert_relative_eq!(greeks.vega, 3256.2375, epsilon = 0.001); + assert_relative_eq!(greeks.rho, 55.8247, epsilon = 0.001); + assert_relative_eq!(greeks.rho_d, -63.3206, epsilon = 0.001); + + assert_relative_eq!( + strategy.calculate_net_delta().net_delta, + -0.1148, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[0], + 0.2492, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[1], + -0.1611, + epsilon = 0.001 + ); + assert!(!strategy.is_delta_neutral()); + assert_eq!(strategy.suggest_delta_adjustments().len(), 2); + + assert_eq!( + strategy.suggest_delta_adjustments()[0], + BuyOptions { + quantity: pos!(0.9213451734695193), + strike: pos!(2800.0), + option_type: OptionStyle::Call + } + ); + + Ok(()) +} diff --git a/tests/unit/strategies/delta/strategy_long_butterfly_spread.rs b/tests/unit/strategies/delta/strategy_long_butterfly_spread.rs new file mode 100644 index 00000000..c5f3fd4c --- /dev/null +++ b/tests/unit/strategies/delta/strategy_long_butterfly_spread.rs @@ -0,0 +1,73 @@ +use approx::assert_relative_eq; +use optionstratlib::greeks::equations::Greeks; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::model::types::{ExpirationDate, OptionStyle}; +use optionstratlib::pos; +use optionstratlib::strategies::butterfly_spread::LongButterflySpread; +use optionstratlib::strategies::delta_neutral::DeltaAdjustment::BuyOptions; +use optionstratlib::strategies::delta_neutral::DeltaNeutrality; +use optionstratlib::utils::logger::setup_logger; +use std::error::Error; + +#[test] +fn test_long_butterfly_spread_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the LongButterflySpread strategy + let underlying_price = pos!(5795.88); + + let strategy = LongButterflySpread::new( + "SP500".to_string(), + underlying_price, // underlying_price + pos!(5710.0), // long_strike_itm + pos!(5780.0), // short_strike + pos!(5850.0), // long_strike_otm + ExpirationDate::Days(2.0), + 0.18, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(1.0), // long quantity + 113.30, // premium_long_low + 64.20, // premium_short + 31.65, // premium_long_high + 0.07, // fees + ); + + let greeks = strategy.greeks(); + + assert_relative_eq!(greeks.delta, -0.0585, epsilon = 0.001); + assert_relative_eq!(greeks.gamma, 0.0168, epsilon = 0.001); + assert_relative_eq!(greeks.theta, -9832.8102, epsilon = 0.001); + assert_relative_eq!(greeks.vega, 991.1053, epsilon = 0.001); + assert_relative_eq!(greeks.rho, 72.3546, epsilon = 0.001); + assert_relative_eq!(greeks.rho_d, -73.3649, epsilon = 0.001); + + assert_relative_eq!( + strategy.calculate_net_delta().net_delta, + -0.0585, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[0], + 0.8744, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[1], + 0.2513, + epsilon = 0.001 + ); + assert!(!strategy.is_delta_neutral()); + assert_eq!(strategy.suggest_delta_adjustments().len(), 2); + + assert_eq!( + strategy.suggest_delta_adjustments()[0], + BuyOptions { + quantity: pos!(0.0669984145182543), + strike: pos!(5710.0), + option_type: OptionStyle::Call + } + ); + + Ok(()) +} diff --git a/tests/unit/strategies/delta/strategy_long_straddle.rs b/tests/unit/strategies/delta/strategy_long_straddle.rs new file mode 100644 index 00000000..5968b42f --- /dev/null +++ b/tests/unit/strategies/delta/strategy_long_straddle.rs @@ -0,0 +1,73 @@ +use approx::assert_relative_eq; +use optionstratlib::greeks::equations::Greeks; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::model::types::{ExpirationDate, OptionStyle}; +use optionstratlib::pos; +use optionstratlib::strategies::delta_neutral::DeltaAdjustment::BuyOptions; +use optionstratlib::strategies::delta_neutral::DeltaNeutrality; +use optionstratlib::strategies::straddle::LongStraddle; +use optionstratlib::utils::logger::setup_logger; +use std::error::Error; + +#[test] +fn test_long_straddle_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the LongStraddle strategy + let underlying_price = pos!(7008.5); + + let strategy = LongStraddle::new( + "CL".to_string(), + underlying_price, // underlying_price + pos!(7140.0), // put_strike + ExpirationDate::Days(45.0), + 0.3745, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(1.0), // quantity + 84.2, // premium_short_call + 353.2, // premium_short_put + 7.0, // open_fee_short_call + 7.01, // close_fee_short_call + 7.01, // open_fee_short_put + 7.01, // close_fee_short_put + ); + + let greeks = strategy.greeks(); + + assert_relative_eq!(greeks.delta, -0.0229, epsilon = 0.001); + assert_relative_eq!(greeks.gamma, 0.0009, epsilon = 0.001); + assert_relative_eq!(greeks.theta, -2935.7343, epsilon = 0.001); + assert_relative_eq!(greeks.vega, 2404.4270, epsilon = 0.001); + assert_relative_eq!(greeks.rho, -111.3740, epsilon = 0.001); + assert_relative_eq!(greeks.rho_d, 19.8110, epsilon = 0.001); + + assert_relative_eq!( + strategy.calculate_net_delta().net_delta, + -0.0229, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[0], + 0.4885, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[1], + -0.5114, + epsilon = 0.001 + ); + assert!(!strategy.is_delta_neutral()); + assert_eq!(strategy.suggest_delta_adjustments().len(), 1); + + assert_eq!( + strategy.suggest_delta_adjustments()[0], + BuyOptions { + quantity: pos!(0.046931443553379394), + strike: pos!(7140.0), + option_type: OptionStyle::Call, + } + ); + + Ok(()) +} diff --git a/tests/unit/strategies/delta/strategy_long_strangle.rs b/tests/unit/strategies/delta/strategy_long_strangle.rs new file mode 100644 index 00000000..47ed33c6 --- /dev/null +++ b/tests/unit/strategies/delta/strategy_long_strangle.rs @@ -0,0 +1,67 @@ +use approx::assert_relative_eq; +use optionstratlib::greeks::equations::Greeks; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::pos; +use optionstratlib::strategies::delta_neutral::DeltaAdjustment::NoAdjustmentNeeded; +use optionstratlib::strategies::delta_neutral::DeltaNeutrality; +use optionstratlib::strategies::strangle::LongStrangle; +use optionstratlib::utils::logger::setup_logger; +use std::error::Error; + +#[test] +fn test_long_strangle_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the LongStrangle strategy + let underlying_price = pos!(7138.5); + + let strategy = LongStrangle::new( + "CL".to_string(), + underlying_price, // underlying_price + pos!(7450.0), // call_strike + pos!(7050.0), // put_strike + ExpirationDate::Days(45.0), + 0.3745, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(1.0), // quantity + 84.2, // premium_short_call + 353.2, // premium_short_put + 7.0, // open_fee_short_call + 7.01, // close_fee_short_call + 7.01, // open_fee_short_put + 7.01, // close_fee_short_put + ); + + let greeks = strategy.greeks(); + + assert_relative_eq!(greeks.delta, -0.0018, epsilon = 0.001); + assert_relative_eq!(greeks.gamma, 0.0008, epsilon = 0.001); + assert_relative_eq!(greeks.theta, -2942.0709, epsilon = 0.001); + assert_relative_eq!(greeks.vega, 2501.9092, epsilon = 0.001); + assert_relative_eq!(greeks.rho, -72.0661, epsilon = 0.001); + assert_relative_eq!(greeks.rho_d, 1.6100, epsilon = 0.001); + + assert_relative_eq!( + strategy.calculate_net_delta().net_delta, + -0.0018, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[0], + 0.4159, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[1], + -0.4178, + epsilon = 0.001 + ); + assert!(strategy.is_delta_neutral()); + assert_eq!(strategy.suggest_delta_adjustments().len(), 1); + + assert_eq!(strategy.suggest_delta_adjustments()[0], NoAdjustmentNeeded); + + Ok(()) +} diff --git a/tests/unit/strategies/delta/strategy_poor_mans_covered_call.rs b/tests/unit/strategies/delta/strategy_poor_mans_covered_call.rs new file mode 100644 index 00000000..392f7db2 --- /dev/null +++ b/tests/unit/strategies/delta/strategy_poor_mans_covered_call.rs @@ -0,0 +1,74 @@ +use approx::assert_relative_eq; +use optionstratlib::greeks::equations::Greeks; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::model::types::{ExpirationDate, OptionStyle}; +use optionstratlib::pos; +use optionstratlib::strategies::delta_neutral::DeltaAdjustment::SellOptions; +use optionstratlib::strategies::delta_neutral::DeltaNeutrality; +use optionstratlib::strategies::poor_mans_covered_call::PoorMansCoveredCall; +use optionstratlib::utils::logger::setup_logger; +use std::error::Error; + +#[test] +fn test_poor_mans_covered_call_integration() -> Result<(), Box> { + setup_logger(); + + let underlying_price = pos!(2703.3); + + let strategy = PoorMansCoveredCall::new( + "GOLD".to_string(), // underlying_symbol + underlying_price, // underlying_price + pos!(2600.0), // long_call_strike + pos!(2800.0), // short_call_strike OTM + ExpirationDate::Days(120.0), // long_call_expiration + ExpirationDate::Days(30.0), // short_call_expiration 30-45 days delta 0.30 or less + 0.17, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(2.0), // quantity + 154.7, // premium_short_call + 30.8, // premium_short_put + 1.74, // open_fee_short_call + 1.74, // close_fee_short_call + 0.85, // open_fee_short_put + 0.85, // close_fee_short_put + ); + + let greeks = strategy.greeks(); + + assert_relative_eq!(greeks.delta, 0.9225, epsilon = 0.001); + assert_relative_eq!(greeks.gamma, 0.0075, epsilon = 0.001); + assert_relative_eq!(greeks.theta, -1043.9572, epsilon = 0.001); + assert_relative_eq!(greeks.vega, 2686.1099, epsilon = 0.001); + assert_relative_eq!(greeks.rho, 1290.9435, epsilon = 0.001); + assert_relative_eq!(greeks.rho_d, -1420.1310, epsilon = 0.001); + + assert_relative_eq!( + strategy.calculate_net_delta().net_delta, + 0.9225, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[0], + 1.4628, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[1], + -0.5402, + epsilon = 0.001 + ); + assert!(!strategy.is_delta_neutral()); + assert_eq!(strategy.suggest_delta_adjustments().len(), 1); + + assert_eq!( + strategy.suggest_delta_adjustments()[0], + SellOptions { + quantity: pos!(3.4154122075924565), + strike: pos!(2800.0), + option_type: OptionStyle::Call + } + ); + + Ok(()) +} diff --git a/tests/unit/strategies/delta/strategy_short_butterfly_spread.rs b/tests/unit/strategies/delta/strategy_short_butterfly_spread.rs new file mode 100644 index 00000000..f8ceae64 --- /dev/null +++ b/tests/unit/strategies/delta/strategy_short_butterfly_spread.rs @@ -0,0 +1,73 @@ +use approx::assert_relative_eq; +use optionstratlib::greeks::equations::Greeks; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::model::types::{ExpirationDate, OptionStyle}; +use optionstratlib::pos; +use optionstratlib::strategies::butterfly_spread::ShortButterflySpread; +use optionstratlib::strategies::delta_neutral::DeltaAdjustment::BuyOptions; +use optionstratlib::strategies::delta_neutral::DeltaNeutrality; +use optionstratlib::utils::logger::setup_logger; +use std::error::Error; + +#[test] +fn test_short_butterfly_spread_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the ShortButterflySpread strategy + let underlying_price = pos!(5781.88); + + let strategy = ShortButterflySpread::new( + "SP500".to_string(), + underlying_price, // underlying_price + pos!(5700.0), // short_strike_itm + pos!(5780.0), // long_strike + pos!(5850.0), // short_strike_otm + ExpirationDate::Days(2.0), + 0.18, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(3.0), // long quantity + 119.01, // premium_long + 66.0, // premium_short + 29.85, // open_fee_long + 4.0, // open_fee_long + ); + + let greeks = strategy.greeks(); + + assert_relative_eq!(greeks.delta, -0.0593, epsilon = 0.001); + assert_relative_eq!(greeks.gamma, 0.0503, epsilon = 0.001); + assert_relative_eq!(greeks.theta, -29062.9106, epsilon = 0.001); + assert_relative_eq!(greeks.vega, 2699.1274, epsilon = 0.001); + assert_relative_eq!(greeks.rho, 197.1329, epsilon = 0.001); + assert_relative_eq!(greeks.rho_d, -199.7983, epsilon = 0.001); + + assert_relative_eq!( + strategy.calculate_net_delta().net_delta, + -0.0593, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[0], + -2.5914, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[1], + -0.5914, + epsilon = 0.001 + ); + assert!(!strategy.is_delta_neutral()); + assert_eq!(strategy.suggest_delta_adjustments().len(), 1); + + assert_eq!( + strategy.suggest_delta_adjustments()[0], + BuyOptions { + quantity: pos!(0.11409430831965134), + strike: pos!(5780.0), + option_type: OptionStyle::Call + } + ); + + Ok(()) +} diff --git a/tests/unit/strategies/delta/strategy_short_straddle.rs b/tests/unit/strategies/delta/strategy_short_straddle.rs new file mode 100644 index 00000000..1af7ecbc --- /dev/null +++ b/tests/unit/strategies/delta/strategy_short_straddle.rs @@ -0,0 +1,73 @@ +use approx::assert_relative_eq; +use optionstratlib::greeks::equations::Greeks; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::model::types::{ExpirationDate, OptionStyle}; +use optionstratlib::pos; +use optionstratlib::strategies::delta_neutral::DeltaAdjustment::SellOptions; +use optionstratlib::strategies::delta_neutral::DeltaNeutrality; +use optionstratlib::strategies::straddle::ShortStraddle; +use optionstratlib::utils::logger::setup_logger; +use std::error::Error; + +#[test] +fn test_short_straddle_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the ShortStraddle strategy + let underlying_price = pos!(7138.5); + + let strategy = ShortStraddle::new( + "CL".to_string(), + underlying_price, // underlying_price + pos!(7140.0), // put_strike + ExpirationDate::Days(45.0), + 0.3745, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(1.0), // quantity + 84.2, // premium_short_call + 353.2, // premium_short_put + 7.01, // open_fee_short_call + 7.01, // close_fee_short_call + 7.01, // open_fee_short_put + 7.01, // close_fee_short_put + ); + + let greeks = strategy.greeks(); + + assert_relative_eq!(greeks.delta, -0.0884, epsilon = 0.001); + assert_relative_eq!(greeks.gamma, 0.0008, epsilon = 0.001); + assert_relative_eq!(greeks.theta, -3012.9912, epsilon = 0.001); + assert_relative_eq!(greeks.vega, 2728.0855, epsilon = 0.001); + assert_relative_eq!(greeks.rho, -14.2856, epsilon = 0.001); + assert_relative_eq!(greeks.rho_d, -77.8057, epsilon = 0.001); + + assert_relative_eq!( + strategy.calculate_net_delta().net_delta, + -0.0884, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[0], + -0.5442, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[1], + 0.4557, + epsilon = 0.001 + ); + assert!(!strategy.is_delta_neutral()); + assert_eq!(strategy.suggest_delta_adjustments().len(), 1); + + assert_eq!( + strategy.suggest_delta_adjustments()[0], + SellOptions { + quantity: pos!(0.1939607389394844), + strike: pos!(7140.0), + option_type: OptionStyle::Put + } + ); + + Ok(()) +} diff --git a/tests/unit/strategies/delta/strategy_short_strangle.rs b/tests/unit/strategies/delta/strategy_short_strangle.rs new file mode 100644 index 00000000..90919da1 --- /dev/null +++ b/tests/unit/strategies/delta/strategy_short_strangle.rs @@ -0,0 +1,67 @@ +use approx::assert_relative_eq; +use optionstratlib::greeks::equations::Greeks; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::pos; +use optionstratlib::strategies::delta_neutral::DeltaAdjustment::NoAdjustmentNeeded; +use optionstratlib::strategies::delta_neutral::DeltaNeutrality; +use optionstratlib::strategies::strangle::ShortStrangle; +use optionstratlib::utils::logger::setup_logger; +use std::error::Error; + +#[test] +fn test_short_strangle_with_greeks_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the ShortStrangle strategy + let underlying_price = pos!(7138.5); + + let strategy = ShortStrangle::new( + "CL".to_string(), + underlying_price, // underlying_price + pos!(7450.0), // call_strike + pos!(7050.0), // put_strike + ExpirationDate::Days(45.0), + 0.3745, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(1.0), // quantity + 84.2, // premium_short_call + 353.2, // premium_short_put + 7.01, // open_fee_short_call + 7.01, // close_fee_short_call + 7.01, // open_fee_short_put + 7.01, // close_fee_short_put + ); + + let greeks = strategy.greeks(); + + assert_relative_eq!(greeks.delta, 0.0018, epsilon = 0.001); + assert_relative_eq!(greeks.gamma, 0.0008, epsilon = 0.001); + assert_relative_eq!(greeks.theta, -2942.0709, epsilon = 0.001); + assert_relative_eq!(greeks.vega, 2501.9092, epsilon = 0.001); + assert_relative_eq!(greeks.rho, -72.0661, epsilon = 0.001); + assert_relative_eq!(greeks.rho_d, 1.6100, epsilon = 0.001); + + assert_relative_eq!( + strategy.calculate_net_delta().net_delta, + 0.0018, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[0], + -0.4159, + epsilon = 0.001 + ); + assert_relative_eq!( + strategy.calculate_net_delta().individual_deltas[1], + 0.4178, + epsilon = 0.001 + ); + assert!(strategy.is_delta_neutral()); + assert_eq!(strategy.suggest_delta_adjustments().len(), 1); + + assert_eq!(strategy.suggest_delta_adjustments()[0], NoAdjustmentNeeded); + + Ok(()) +} diff --git a/tests/unit/strategies/mod.rs b/tests/unit/strategies/mod.rs new file mode 100644 index 00000000..51373fc9 --- /dev/null +++ b/tests/unit/strategies/mod.rs @@ -0,0 +1,8 @@ +/****************************************************************************** + Author: Joaquín Béjar García + Email: jb@taunais.com + Date: 18/12/24 +******************************************************************************/ +mod delta; +mod simple; +mod optimal; diff --git a/tests/unit/strategies/optimal/mod.rs b/tests/unit/strategies/optimal/mod.rs new file mode 100644 index 00000000..2b9bb896 --- /dev/null +++ b/tests/unit/strategies/optimal/mod.rs @@ -0,0 +1,20 @@ +/****************************************************************************** + Author: Joaquín Béjar García + Email: jb@taunais.com + Date: 19/12/24 + ******************************************************************************/ +mod strategy_bear_call_spread; +mod strategy_short_strangle; +mod strategy_short_straddle; +mod strategy_short_butterfly_spread; +mod strategy_poor_mans_covered_call; +mod strategy_long_strangle; +mod strategy_long_straddle; +mod strategy_long_butterfly_spread; +mod strategy_iron_condor; +mod strategy_iron_butterfly; +mod strategy_custom; +mod strategy_bull_put_spread; +mod strategy_call_butterfly; +mod strategy_bull_call_spread; +mod strategy_bear_put_spread; \ No newline at end of file diff --git a/tests/unit/strategies/optimal/strategy_bear_call_spread.rs b/tests/unit/strategies/optimal/strategy_bear_call_spread.rs new file mode 100644 index 00000000..97fda409 --- /dev/null +++ b/tests/unit/strategies/optimal/strategy_bear_call_spread.rs @@ -0,0 +1,43 @@ +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::pos; +use optionstratlib::strategies::base::{Optimizable, Strategies}; +use optionstratlib::strategies::bear_call_spread::BearCallSpread; +use optionstratlib::utils::logger::setup_logger; +use std::error::Error; +use approx::assert_relative_eq; +use optionstratlib::chains::chain::OptionChain; +use optionstratlib::strategies::utils::FindOptimalSide; + +#[test] +fn test_bear_call_spread_integration() -> Result<(), Box> { + setup_logger(); + // Define inputs for the BearCallSpread strategy + let underlying_price = pos!(5781.88); + let mut strategy = BearCallSpread::new( + "SP500".to_string(), + underlying_price, // underlying_price + pos!(5750.0), // long_strike_itm + pos!(5820.0), // short_strike + ExpirationDate::Days(2.0), + 0.18, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(2.0), // long quantity + 85.04, // premium_long + 29.85, // premium_short + 0.78, // open_fee_long + 0.78, // open_fee_long + 0.73, // close_fee_long + 0.73, // close_fee_short + ); + + let option_chain = + OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; + strategy.best_area(&option_chain, FindOptimalSide::All); + assert_relative_eq!(strategy.profit_area(), 730.2965, epsilon = 0.001); + strategy.best_ratio(&option_chain, FindOptimalSide::Upper); + assert_relative_eq!(strategy.profit_ratio(), 65.8833, epsilon = 0.001); + + Ok(()) +} diff --git a/tests/unit/strategies/optimal/strategy_bear_put_spread.rs b/tests/unit/strategies/optimal/strategy_bear_put_spread.rs new file mode 100644 index 00000000..2753f5c1 --- /dev/null +++ b/tests/unit/strategies/optimal/strategy_bear_put_spread.rs @@ -0,0 +1,45 @@ +use approx::assert_relative_eq; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::strategies::base::{Optimizable, Strategies}; +use optionstratlib::strategies::bear_put_spread::BearPutSpread; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::pos; +use std::error::Error; +use optionstratlib::chains::chain::OptionChain; +use optionstratlib::strategies::utils::FindOptimalSide; + +#[test] +fn test_bear_put_spread_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the BearPutSpread strategy + let underlying_price = pos!(5781.88); + + let mut strategy = BearPutSpread::new( + "SP500".to_string(), + underlying_price, // underlying_price + pos!(5850.0), // long_strike + pos!(5720.0), // short_strike + ExpirationDate::Days(2.0), + 0.18, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(2.0), // long quantity + 85.04, // premium_long + 29.85, // premium_short + 0.78, // open_fee_long + 0.78, // open_fee_long + 0.73, // close_fee_long + 0.73, // close_fee_short + ); + + let option_chain = + OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; + strategy.best_area(&option_chain, FindOptimalSide::All); + assert_relative_eq!(strategy.profit_area(), 741.8541, epsilon = 0.001); + strategy.best_ratio(&option_chain, FindOptimalSide::Upper); + assert_relative_eq!(strategy.profit_ratio(), 66.6666, epsilon = 0.001); + + Ok(()) +} diff --git a/tests/unit/strategies/optimal/strategy_bull_call_spread.rs b/tests/unit/strategies/optimal/strategy_bull_call_spread.rs new file mode 100644 index 00000000..34cabae6 --- /dev/null +++ b/tests/unit/strategies/optimal/strategy_bull_call_spread.rs @@ -0,0 +1,45 @@ +use approx::assert_relative_eq; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::strategies::base::{Optimizable, Strategies}; +use optionstratlib::strategies::bull_call_spread::BullCallSpread; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::pos; +use std::error::Error; +use optionstratlib::chains::chain::OptionChain; +use optionstratlib::strategies::utils::FindOptimalSide; + +#[test] +fn test_bull_call_spread_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the BullCallSpread strategy + let underlying_price = pos!(5781.88); + + let mut strategy = BullCallSpread::new( + "SP500".to_string(), + underlying_price, // underlying_price + pos!(5750.0), // long_strike_itm + pos!(5820.0), // short_strike + ExpirationDate::Days(2.0), + 0.18, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(2.0), // long quantity + 85.04, // premium_long + 29.85, // premium_short + 0.78, // open_fee_long + 0.78, // open_fee_long + 0.73, // close_fee_long + 0.73, // close_fee_short + ); + + let option_chain = + OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; + strategy.best_area(&option_chain, FindOptimalSide::All); + assert_relative_eq!(strategy.profit_area(), 417.3849, epsilon = 0.001); + strategy.best_ratio(&option_chain, FindOptimalSide::Upper); + assert_relative_eq!(strategy.profit_ratio(), 473.3944, epsilon = 0.001); + + Ok(()) +} diff --git a/tests/unit/strategies/optimal/strategy_bull_put_spread.rs b/tests/unit/strategies/optimal/strategy_bull_put_spread.rs new file mode 100644 index 00000000..e2ecf3d9 --- /dev/null +++ b/tests/unit/strategies/optimal/strategy_bull_put_spread.rs @@ -0,0 +1,45 @@ +use approx::assert_relative_eq; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::strategies::base::{Optimizable, Strategies}; +use optionstratlib::strategies::bull_put_spread::BullPutSpread; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::pos; +use std::error::Error; +use optionstratlib::chains::chain::OptionChain; +use optionstratlib::strategies::utils::FindOptimalSide; + +#[test] +fn test_bull_put_spread_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the BullPutSpread strategy + let underlying_price = pos!(5781.88); + + let mut strategy = BullPutSpread::new( + "SP500".to_string(), + underlying_price, // underlying_price + pos!(5750.0), // long_strike_itm + pos!(5920.0), // short_strike + ExpirationDate::Days(2.0), + 0.18, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(2.0), // long quantity + 15.04, // premium_long + 89.85, // premium_short + 0.78, // open_fee_long + 0.78, // open_fee_long + 0.73, // close_fee_long + 0.73, // close_fee_short + ); + + let option_chain = + OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; + strategy.best_area(&option_chain, FindOptimalSide::All); + assert_relative_eq!(strategy.profit_area(), 1584.9157, epsilon = 0.001); + strategy.best_ratio(&option_chain, FindOptimalSide::Upper); + assert_relative_eq!(strategy.profit_ratio(), 2115.6573, epsilon = 0.001); + + Ok(()) +} diff --git a/tests/unit/strategies/optimal/strategy_call_butterfly.rs b/tests/unit/strategies/optimal/strategy_call_butterfly.rs new file mode 100644 index 00000000..2cef73e7 --- /dev/null +++ b/tests/unit/strategies/optimal/strategy_call_butterfly.rs @@ -0,0 +1,49 @@ +use approx::assert_relative_eq; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::strategies::base::{Optimizable, Strategies}; +use optionstratlib::strategies::call_butterfly::CallButterfly; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::pos; +use std::error::Error; +use optionstratlib::chains::chain::OptionChain; +use optionstratlib::strategies::utils::FindOptimalSide; + +#[test] +fn test_call_butterfly_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the CallButterfly strategy + let underlying_price = pos!(5781.88); + + let mut strategy = CallButterfly::new( + "SP500".to_string(), + underlying_price, // underlying_price + pos!(5750.0), // long_call_strike + pos!(5800.0), // short_call_low_strike + pos!(5850.0), // short_call_high_strike + ExpirationDate::Days(2.0), + 0.18, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(1.0), // long quantity + 85.04, // premium_long_itm + 53.04, // premium_long_otm + 28.85, // premium_short + 0.78, // premium_short + 0.78, // open_fee_long + 0.78, // close_fee_long + 0.73, // close_fee_short + 0.73, // close_fee_short + 0.73, // open_fee_short + ); + + let option_chain = + OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; + strategy.best_area(&option_chain, FindOptimalSide::All); + assert_relative_eq!(strategy.profit_area(), 67071.5408, epsilon = 0.001); + strategy.best_ratio(&option_chain, FindOptimalSide::Upper); + assert_relative_eq!(strategy.profit_ratio(), 5340.0, epsilon = 0.001); + + Ok(()) +} diff --git a/tests/unit/strategies/optimal/strategy_custom.rs b/tests/unit/strategies/optimal/strategy_custom.rs new file mode 100644 index 00000000..afcf6e1a --- /dev/null +++ b/tests/unit/strategies/optimal/strategy_custom.rs @@ -0,0 +1,92 @@ +use approx::assert_relative_eq; +use chrono::Utc; +use optionstratlib::model::option::Options; +use optionstratlib::model::position::Position; +use optionstratlib::model::types::{ExpirationDate, OptionStyle, OptionType, PositiveF64, Side}; +use optionstratlib::pos; +use optionstratlib::strategies::base::{Optimizable, Strategies}; +use optionstratlib::strategies::custom::CustomStrategy; +use optionstratlib::utils::logger::setup_logger; +use std::error::Error; +use tracing::info; +use optionstratlib::chains::chain::OptionChain; +use optionstratlib::strategies::utils::FindOptimalSide; + +#[test] +fn test_custom_strategy_integration() -> Result<(), Box> { + setup_logger(); + + // Define common parameters + let underlying_price = pos!(2340.0); + let underlying_symbol = "GAS".to_string(); + let expiration = ExpirationDate::Days(6.0); + let implied_volatility = 0.73; + let risk_free_rate = 0.05; + let dividend_yield = 0.0; + + // Create positions + let positions = vec![ + Position::new( + Options::new( + OptionType::European, + Side::Long, + underlying_symbol.clone(), + pos!(2100.0), + expiration.clone(), + implied_volatility, + pos!(2.0), + underlying_price, + risk_free_rate, + OptionStyle::Call, + dividend_yield, + None, + ), + 192.0, + Utc::now(), + 7.51, + 7.51, + ), + Position::new( + Options::new( + OptionType::European, + Side::Short, + underlying_symbol.clone(), + pos!(2250.0), + expiration.clone(), + implied_volatility, + pos!(2.0), + underlying_price, + risk_free_rate, + OptionStyle::Put, + dividend_yield, + None, + ), + 88.0, + Utc::now(), + 6.68, + 6.68, + ), + ]; + + let mut strategy = CustomStrategy::new( + "Custom Strategy".to_string(), + underlying_symbol, + "Example of a custom strategy".to_string(), + underlying_price, + positions, + 0.01, + 10, + 0.1, + ); + + let option_chain = + OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; + strategy.best_area(&option_chain, FindOptimalSide::Lower); + info!("Profit Area: {:.4}", strategy.profit_area()); + assert_relative_eq!(strategy.profit_area(), 75.4005, epsilon = 0.001); + strategy.best_ratio(&option_chain, FindOptimalSide::All); + info!("Profit Ratio: {:.4}", strategy.profit_ratio()); + assert_relative_eq!(strategy.profit_ratio(), 15.0989, epsilon = 0.001); + + Ok(()) +} diff --git a/tests/unit/strategies/optimal/strategy_iron_butterfly.rs b/tests/unit/strategies/optimal/strategy_iron_butterfly.rs new file mode 100644 index 00000000..52972fbd --- /dev/null +++ b/tests/unit/strategies/optimal/strategy_iron_butterfly.rs @@ -0,0 +1,46 @@ +use approx::assert_relative_eq; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::strategies::base::{Optimizable, Strategies}; +use optionstratlib::strategies::iron_butterfly::IronButterfly; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::pos; +use std::error::Error; +use optionstratlib::chains::chain::OptionChain; +use optionstratlib::strategies::utils::FindOptimalSide; + +#[test] +fn test_iron_butterfly_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the IronButterfly strategy + let underlying_price = pos!(2646.9); + + let mut strategy = IronButterfly::new( + "GOLD".to_string(), + underlying_price, // underlying_price + pos!(2725.0), // short_call_strike + pos!(2800.0), // long_call_strike + pos!(2500.0), // long_put_strike + ExpirationDate::Days(30.0), + 0.1548, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(2.0), // quantity + 38.8, // premium_short_call + 30.4, // premium_short_put + 23.3, // premium_long_call + 16.8, // premium_long_put + 0.96, // open_fee + 0.96, // close_fee + ); + + let option_chain = + OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; + strategy.best_area(&option_chain, FindOptimalSide::All); + assert_relative_eq!(strategy.profit_area(), 23.3347, epsilon = 0.001); + strategy.best_ratio(&option_chain, FindOptimalSide::Upper); + assert_relative_eq!(strategy.profit_ratio(), 387.3294, epsilon = 0.001); + + Ok(()) +} diff --git a/tests/unit/strategies/optimal/strategy_iron_condor.rs b/tests/unit/strategies/optimal/strategy_iron_condor.rs new file mode 100644 index 00000000..b39de642 --- /dev/null +++ b/tests/unit/strategies/optimal/strategy_iron_condor.rs @@ -0,0 +1,47 @@ +use approx::assert_relative_eq; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::strategies::base::{Optimizable, Strategies}; +use optionstratlib::strategies::iron_condor::IronCondor; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::pos; +use std::error::Error; +use optionstratlib::chains::chain::OptionChain; +use optionstratlib::strategies::utils::FindOptimalSide; + +#[test] +fn test_iron_condor_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the IronCondor strategy + let underlying_price = pos!(2646.9); + + let mut strategy = IronCondor::new( + "GOLD".to_string(), + underlying_price, // underlying_price + pos!(2725.0), // short_call_strike + pos!(2560.0), // short_put_strike + pos!(2800.0), // long_call_strike + pos!(2500.0), // long_put_strike + ExpirationDate::Days(30.0), + 0.1548, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(2.0), // quantity + 38.8, // premium_short_call + 30.4, // premium_short_put + 23.3, // premium_long_call + 16.8, // premium_long_put + 0.96, // open_fee + 0.96, // close_fee + ); + + let option_chain = + OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; + strategy.best_area(&option_chain, FindOptimalSide::All); + assert_relative_eq!(strategy.profit_area(), 19.5798, epsilon = 0.001); + strategy.best_ratio(&option_chain, FindOptimalSide::Upper); + assert_relative_eq!(strategy.profit_ratio(), 237.3819, epsilon = 0.001); + + Ok(()) +} diff --git a/tests/unit/strategies/optimal/strategy_long_butterfly_spread.rs b/tests/unit/strategies/optimal/strategy_long_butterfly_spread.rs new file mode 100644 index 00000000..14eac3d9 --- /dev/null +++ b/tests/unit/strategies/optimal/strategy_long_butterfly_spread.rs @@ -0,0 +1,44 @@ +use approx::assert_relative_eq; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::strategies::base::{Optimizable, Strategies}; +use optionstratlib::strategies::butterfly_spread::LongButterflySpread; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::pos; +use std::error::Error; +use optionstratlib::chains::chain::OptionChain; +use optionstratlib::strategies::utils::FindOptimalSide; + +#[test] +fn test_long_butterfly_spread_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the LongButterflySpread strategy + let underlying_price = pos!(5795.88); + + let mut strategy = LongButterflySpread::new( + "SP500".to_string(), + underlying_price, // underlying_price + pos!(5710.0), // long_strike_itm + pos!(5780.0), // short_strike + pos!(5850.0), // long_strike_otm + ExpirationDate::Days(2.0), + 0.18, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(1.0), // long quantity + 113.30, // premium_long_low + 64.20, // premium_short + 31.65, // premium_long_high + 0.07, // fees + ); + + let option_chain = + OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; + strategy.best_area(&option_chain, FindOptimalSide::All); + assert_relative_eq!(strategy.profit_area(), 399.5201, epsilon = 0.001); + strategy.best_ratio(&option_chain, FindOptimalSide::Upper); + assert_relative_eq!(strategy.profit_ratio(), 1793.9393, epsilon = 0.001); + + Ok(()) +} diff --git a/tests/unit/strategies/optimal/strategy_long_straddle.rs b/tests/unit/strategies/optimal/strategy_long_straddle.rs new file mode 100644 index 00000000..7861d38a --- /dev/null +++ b/tests/unit/strategies/optimal/strategy_long_straddle.rs @@ -0,0 +1,44 @@ +use approx::assert_relative_eq; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::strategies::base::{Optimizable, Strategies}; +use optionstratlib::strategies::straddle::LongStraddle; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::pos; +use std::error::Error; +use optionstratlib::chains::chain::OptionChain; +use optionstratlib::strategies::utils::FindOptimalSide; + +#[test] +fn test_long_straddle_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the LongStraddle strategy + let underlying_price = pos!(7008.5); + + let mut strategy = LongStraddle::new( + "CL".to_string(), + underlying_price, // underlying_price + pos!(7140.0), // put_strike + ExpirationDate::Days(45.0), + 0.3745, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(1.0), // quantity + 84.2, // premium_short_call + 353.2, // premium_short_put + 7.0, // open_fee_short_call + 7.01, // close_fee_short_call + 7.01, // open_fee_short_put + 7.01, // close_fee_short_put + ); + + let option_chain = + OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; + strategy.best_area(&option_chain, FindOptimalSide::All); + assert_relative_eq!(strategy.profit_area(), 414.2530, epsilon = 0.001); + strategy.best_ratio(&option_chain, FindOptimalSide::Upper); + assert_relative_eq!(strategy.profit_ratio(), 200.0, epsilon = 0.001); + + Ok(()) +} diff --git a/tests/unit/strategies/optimal/strategy_long_strangle.rs b/tests/unit/strategies/optimal/strategy_long_strangle.rs new file mode 100644 index 00000000..0db52ef5 --- /dev/null +++ b/tests/unit/strategies/optimal/strategy_long_strangle.rs @@ -0,0 +1,45 @@ +use approx::assert_relative_eq; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::strategies::base::{Optimizable, Strategies}; +use optionstratlib::strategies::strangle::LongStrangle; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::pos; +use std::error::Error; +use optionstratlib::chains::chain::OptionChain; +use optionstratlib::strategies::utils::FindOptimalSide; + +#[test] +fn test_long_strangle_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the LongStrangle strategy + let underlying_price = pos!(7138.5); + + let mut strategy = LongStrangle::new( + "CL".to_string(), + underlying_price, // underlying_price + pos!(7450.0), // call_strike + pos!(7050.0), // put_strike + ExpirationDate::Days(45.0), + 0.3745, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(1.0), // quantity + 84.2, // premium_short_call + 353.2, // premium_short_put + 7.0, // open_fee_short_call + 7.01, // close_fee_short_call + 7.01, // open_fee_short_put + 7.01, // close_fee_short_put + ); + + let option_chain = + OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; + strategy.best_area(&option_chain, FindOptimalSide::All); + assert_relative_eq!(strategy.profit_area(), 0.2439, epsilon = 0.001); + strategy.best_ratio(&option_chain, FindOptimalSide::Upper); + assert_relative_eq!(strategy.profit_ratio(), 0.0518, epsilon = 0.001); + + Ok(()) +} diff --git a/tests/unit/strategies/optimal/strategy_poor_mans_covered_call.rs b/tests/unit/strategies/optimal/strategy_poor_mans_covered_call.rs new file mode 100644 index 00000000..a5ab09e7 --- /dev/null +++ b/tests/unit/strategies/optimal/strategy_poor_mans_covered_call.rs @@ -0,0 +1,45 @@ +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::strategies::base::{Optimizable, Strategies}; +use optionstratlib::strategies::poor_mans_covered_call::PoorMansCoveredCall; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::pos; +use std::error::Error; +use approx::assert_relative_eq; +use optionstratlib::chains::chain::OptionChain; +use optionstratlib::strategies::utils::FindOptimalSide; + +#[test] +fn test_poor_mans_covered_call_integration() -> Result<(), Box> { + setup_logger(); + + let underlying_price = pos!(2703.3); + + let mut strategy = PoorMansCoveredCall::new( + "GOLD".to_string(), // underlying_symbol + underlying_price, // underlying_price + pos!(2600.0), // long_call_strike + pos!(2800.0), // short_call_strike OTM + ExpirationDate::Days(120.0), // long_call_expiration + ExpirationDate::Days(30.0), // short_call_expiration 30-45 days delta 0.30 or less + 0.17, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(2.0), // quantity + 154.7, // premium_short_call + 30.8, // premium_short_put + 1.74, // open_fee_short_call + 1.74, // close_fee_short_call + 0.85, // open_fee_short_put + 0.85, // close_fee_short_put + ); + + let option_chain = + OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; + strategy.best_area(&option_chain, FindOptimalSide::All); + assert_relative_eq!(strategy.profit_area(), 817.2115, epsilon = 0.001); + strategy.best_ratio(&option_chain, FindOptimalSide::Upper); + assert_relative_eq!(strategy.profit_ratio(), 408.9058, epsilon = 0.001); + + Ok(()) +} diff --git a/tests/unit/strategies/optimal/strategy_short_butterfly_spread.rs b/tests/unit/strategies/optimal/strategy_short_butterfly_spread.rs new file mode 100644 index 00000000..2b6c3736 --- /dev/null +++ b/tests/unit/strategies/optimal/strategy_short_butterfly_spread.rs @@ -0,0 +1,44 @@ +use approx::assert_relative_eq; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::strategies::base::{Optimizable, Strategies}; +use optionstratlib::strategies::butterfly_spread::ShortButterflySpread; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::pos; +use std::error::Error; +use optionstratlib::chains::chain::OptionChain; +use optionstratlib::strategies::utils::FindOptimalSide; + +#[test] +fn test_short_butterfly_spread_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the ShortButterflySpread strategy + let underlying_price = pos!(5781.88); + + let mut strategy = ShortButterflySpread::new( + "SP500".to_string(), + underlying_price, // underlying_price + pos!(5700.0), // short_strike_itm + pos!(5780.0), // long_strike + pos!(5850.0), // short_strike_otm + ExpirationDate::Days(2.0), + 0.18, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(3.0), // long quantity + 119.01, // premium_long + 66.0, // premium_short + 29.85, // open_fee_long + 4.0, // open_fee_long + ); + + let option_chain = + OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; + strategy.best_area(&option_chain, FindOptimalSide::All); + assert_relative_eq!(strategy.profit_area(), 778.4392, epsilon = 0.001); + strategy.best_ratio(&option_chain, FindOptimalSide::Upper); + assert_relative_eq!(strategy.profit_ratio(), 535.8086, epsilon = 0.001); + + Ok(()) +} diff --git a/tests/unit/strategies/optimal/strategy_short_straddle.rs b/tests/unit/strategies/optimal/strategy_short_straddle.rs new file mode 100644 index 00000000..c9b73a29 --- /dev/null +++ b/tests/unit/strategies/optimal/strategy_short_straddle.rs @@ -0,0 +1,44 @@ +use approx::assert_relative_eq; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::strategies::base::{Optimizable, Strategies}; +use optionstratlib::strategies::straddle::ShortStraddle; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::pos; +use std::error::Error; +use optionstratlib::chains::chain::OptionChain; +use optionstratlib::strategies::utils::FindOptimalSide; + +#[test] +fn test_short_straddle_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the ShortStraddle strategy + let underlying_price = pos!(7138.5); + + let mut strategy = ShortStraddle::new( + "CL".to_string(), + underlying_price, // underlying_price + pos!(7140.0), // put_strike + ExpirationDate::Days(45.0), + 0.3745, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(1.0), // quantity + 84.2, // premium_short_call + 353.2, // premium_short_put + 7.01, // open_fee_short_call + 7.01, // close_fee_short_call + 7.01, // open_fee_short_put + 7.01, // close_fee_short_put + ); + + let option_chain = + OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; + strategy.best_area(&option_chain, FindOptimalSide::All); + assert_relative_eq!(strategy.profit_area(), 58.7383, epsilon = 0.001); + strategy.best_ratio(&option_chain, FindOptimalSide::Upper); + assert_relative_eq!(strategy.profit_ratio(), 50.0, epsilon = 0.001); + + Ok(()) +} diff --git a/tests/unit/strategies/optimal/strategy_short_strangle.rs b/tests/unit/strategies/optimal/strategy_short_strangle.rs new file mode 100644 index 00000000..604ae448 --- /dev/null +++ b/tests/unit/strategies/optimal/strategy_short_strangle.rs @@ -0,0 +1,45 @@ +use approx::assert_relative_eq; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::strategies::base::{Optimizable, Strategies}; +use optionstratlib::strategies::strangle::ShortStrangle; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::pos; +use std::error::Error; +use optionstratlib::chains::chain::OptionChain; +use optionstratlib::strategies::utils::FindOptimalSide; + +#[test] +fn test_short_strangle_with_greeks_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the ShortStrangle strategy + let underlying_price = pos!(7138.5); + + let mut strategy = ShortStrangle::new( + "CL".to_string(), + underlying_price, // underlying_price + pos!(7450.0), // call_strike + pos!(7050.0), // put_strike + ExpirationDate::Days(45.0), + 0.3745, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(1.0), // quantity + 84.2, // premium_short_call + 353.2, // premium_short_put + 7.01, // open_fee_short_call + 7.01, // close_fee_short_call + 7.01, // open_fee_short_put + 7.01, // close_fee_short_put + ); + + let option_chain = + OptionChain::load_from_json("./examples/Chains/SP500-18-oct-2024-5781.88.json")?; + strategy.best_area(&option_chain, FindOptimalSide::All); + assert_relative_eq!(strategy.profit_area(), 9.8081, epsilon = 0.001); + strategy.best_ratio(&option_chain, FindOptimalSide::Upper); + assert_relative_eq!(strategy.profit_ratio(), 47.4665, epsilon = 0.001); + + Ok(()) +} diff --git a/tests/unit/strategies/simple/mod.rs b/tests/unit/strategies/simple/mod.rs new file mode 100644 index 00000000..8ad059a9 --- /dev/null +++ b/tests/unit/strategies/simple/mod.rs @@ -0,0 +1,21 @@ +/****************************************************************************** + Author: Joaquín Béjar García + Email: jb@taunais.com + Date: 18/12/24 +******************************************************************************/ +mod strategy_bear_call_spread; +mod strategy_bear_put_spread; +mod strategy_bull_call_spread; +mod strategy_bull_put_spread; +mod strategy_call_butterfly; +mod strategy_custom; +mod strategy_graph; +mod strategy_iron_butterfly; +mod strategy_iron_condor; +mod strategy_long_butterfly_spread; +mod strategy_long_straddle; +mod strategy_long_strangle; +mod strategy_poor_mans_covered_call; +mod strategy_short_butterfly_spread; +mod strategy_short_straddle; +mod strategy_short_strangle; diff --git a/tests/unit/strategies/simple/strategy_bear_call_spread.rs b/tests/unit/strategies/simple/strategy_bear_call_spread.rs new file mode 100644 index 00000000..b2387e69 --- /dev/null +++ b/tests/unit/strategies/simple/strategy_bear_call_spread.rs @@ -0,0 +1,48 @@ +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::PositiveF64; +use optionstratlib::pos; +use optionstratlib::strategies::base::Strategies; +use optionstratlib::strategies::bear_call_spread::BearCallSpread; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::visualization::utils::Graph; +use std::error::Error; + +#[test] +fn test_bear_call_spread_integration() -> Result<(), Box> { + setup_logger(); + // Define inputs for the BearCallSpread strategy + let underlying_price = pos!(5781.88); + + let strategy = BearCallSpread::new( + "SP500".to_string(), + underlying_price, // underlying_price + pos!(5750.0), // long_strike_itm + pos!(5820.0), // short_strike + ExpirationDate::Days(2.0), + 0.18, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(2.0), // long quantity + 85.04, // premium_long + 29.85, // premium_short + 0.78, // open_fee_long + 0.78, // open_fee_long + 0.73, // close_fee_long + 0.73, // close_fee_short + ); + + // Assertions to validate strategy properties and computations + assert_eq!(strategy.title(), "Bear Call Spread Strategy:\n\tUnderlying: SP500 @ $5750 Short Call European Option\n\tUnderlying: SP500 @ $5820 Long Call European Option"); + assert_eq!(strategy.get_break_even_points().len(), 1); + assert_eq!(strategy.net_premium_received(), 104.34); + assert!(strategy.max_profit().is_ok()); + assert!(strategy.max_loss().is_ok()); + assert_eq!(strategy.max_profit()?, pos!(104.34)); + assert_eq!(strategy.max_loss()?, pos!(35.66)); + assert_eq!(strategy.total_cost(), pos!(229.58)); + assert_eq!(strategy.fees(), 3.02); + assert!(strategy.profit_area() > 0.0); + assert!(strategy.profit_ratio() > 0.0); + + Ok(()) +} diff --git a/tests/unit/strategies/simple/strategy_bear_put_spread.rs b/tests/unit/strategies/simple/strategy_bear_put_spread.rs new file mode 100644 index 00000000..d30a2d87 --- /dev/null +++ b/tests/unit/strategies/simple/strategy_bear_put_spread.rs @@ -0,0 +1,55 @@ +use approx::assert_relative_eq; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::{PositiveF64, PZERO}; +use optionstratlib::strategies::base::Strategies; +use optionstratlib::strategies::bear_put_spread::BearPutSpread; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::visualization::utils::Graph; +use optionstratlib::{assert_positivef64_relative_eq, pos}; +use std::error::Error; + +#[test] +fn test_bear_put_spread_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the BearPutSpread strategy + let underlying_price = pos!(5781.88); + + let strategy = BearPutSpread::new( + "SP500".to_string(), + underlying_price, // underlying_price + pos!(5850.0), // long_strike + pos!(5720.0), // short_strike + ExpirationDate::Days(2.0), + 0.18, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(2.0), // long quantity + 85.04, // premium_long + 29.85, // premium_short + 0.78, // open_fee_long + 0.78, // open_fee_long + 0.73, // close_fee_long + 0.73, // close_fee_short + ); + + // Assertions to validate strategy properties and computations + assert_eq!(strategy.title(), "Bear Put Spread Strategy:\n\tUnderlying: SP500 @ $5850 Long Put European Option\n\tUnderlying: SP500 @ $5720 Short Put European Option"); + assert_eq!(strategy.get_break_even_points().len(), 1); + assert_relative_eq!(strategy.net_premium_received(), 116.42, epsilon = 0.001); + assert!(strategy.max_profit().is_ok()); + assert!(strategy.max_loss().is_ok()); + assert_positivef64_relative_eq!(strategy.max_loss()?, pos!(116.42), pos!(0.0001)); + assert_positivef64_relative_eq!(strategy.total_cost(), pos!(116.42), pos!(0.0001)); + assert_eq!(strategy.fees(), 3.02); + assert!(strategy.profit_area() > 0.0); + assert!(strategy.profit_ratio() > 0.0); + + // Validate price range calculations + let price_range = strategy.best_range_to_show(pos!(1.0)).unwrap(); + assert!(!price_range.is_empty()); + assert!(price_range[0] < strategy.get_break_even_points()[0]); + assert!(price_range[price_range.len() - 1] > strategy.get_break_even_points()[0]); + + Ok(()) +} diff --git a/tests/unit/strategies/simple/strategy_bull_call_spread.rs b/tests/unit/strategies/simple/strategy_bull_call_spread.rs new file mode 100644 index 00000000..a2cf487c --- /dev/null +++ b/tests/unit/strategies/simple/strategy_bull_call_spread.rs @@ -0,0 +1,55 @@ +use approx::assert_relative_eq; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::{PositiveF64, PZERO}; +use optionstratlib::strategies::base::Strategies; +use optionstratlib::strategies::bull_call_spread::BullCallSpread; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::visualization::utils::Graph; +use optionstratlib::{assert_positivef64_relative_eq, pos}; +use std::error::Error; + +#[test] +fn test_bull_call_spread_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the BullCallSpread strategy + let underlying_price = pos!(5781.88); + + let strategy = BullCallSpread::new( + "SP500".to_string(), + underlying_price, // underlying_price + pos!(5750.0), // long_strike_itm + pos!(5820.0), // short_strike + ExpirationDate::Days(2.0), + 0.18, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(2.0), // long quantity + 85.04, // premium_long + 29.85, // premium_short + 0.78, // open_fee_long + 0.78, // open_fee_long + 0.73, // close_fee_long + 0.73, // close_fee_short + ); + + // Assertions to validate strategy properties and computations + assert_eq!(strategy.title(), "Bull Call Spread Strategy:\n\tUnderlying: SP500 @ $5750 Long Call European Option\n\tUnderlying: SP500 @ $5820 Short Call European Option"); + assert_eq!(strategy.get_break_even_points().len(), 1); + assert_relative_eq!(strategy.net_premium_received(), -116.42, epsilon = 0.001); + assert!(strategy.max_profit().is_ok()); + assert!(strategy.max_loss().is_ok()); + assert_positivef64_relative_eq!(strategy.max_loss()?, pos!(116.42), pos!(0.0001)); + assert_positivef64_relative_eq!(strategy.total_cost(), pos!(229.98), pos!(0.0001)); + assert_eq!(strategy.fees(), 3.02); + assert!(strategy.profit_area() > 0.0); + assert!(strategy.profit_ratio() > 0.0); + + // Validate price range calculations + let price_range = strategy.best_range_to_show(pos!(1.0)).unwrap(); + assert!(!price_range.is_empty()); + assert!(price_range[0] < strategy.get_break_even_points()[0]); + assert!(price_range[price_range.len() - 1] > strategy.get_break_even_points()[0]); + + Ok(()) +} diff --git a/tests/unit/strategies/simple/strategy_bull_put_spread.rs b/tests/unit/strategies/simple/strategy_bull_put_spread.rs new file mode 100644 index 00000000..19d9fad0 --- /dev/null +++ b/tests/unit/strategies/simple/strategy_bull_put_spread.rs @@ -0,0 +1,55 @@ +use approx::assert_relative_eq; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::{PositiveF64, PZERO}; +use optionstratlib::strategies::base::Strategies; +use optionstratlib::strategies::bull_put_spread::BullPutSpread; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::visualization::utils::Graph; +use optionstratlib::{assert_positivef64_relative_eq, pos}; +use std::error::Error; + +#[test] +fn test_bull_put_spread_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the BullPutSpread strategy + let underlying_price = pos!(5781.88); + + let strategy = BullPutSpread::new( + "SP500".to_string(), + underlying_price, // underlying_price + pos!(5750.0), // long_strike_itm + pos!(5920.0), // short_strike + ExpirationDate::Days(2.0), + 0.18, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(2.0), // long quantity + 15.04, // premium_long + 89.85, // premium_short + 0.78, // open_fee_long + 0.78, // open_fee_long + 0.73, // close_fee_long + 0.73, // close_fee_short + ); + + // Assertions to validate strategy properties and computations + assert_eq!(strategy.title(), "Bull Put Spread Strategy:\n\tUnderlying: SP500 @ $5750 Long Put European Option\n\tUnderlying: SP500 @ $5920 Short Put European Option"); + assert_eq!(strategy.get_break_even_points().len(), 1); + assert_relative_eq!(strategy.net_premium_received(), 143.58, epsilon = 0.001); + assert!(strategy.max_profit().is_ok()); + assert!(strategy.max_loss().is_ok()); + assert_positivef64_relative_eq!(strategy.max_profit()?, pos!(143.58), pos!(0.0001)); + assert_positivef64_relative_eq!(strategy.total_cost(), pos!(209.98), pos!(0.0001)); + assert_eq!(strategy.fees(), 3.02); + assert!(strategy.profit_area() > 0.0); + assert!(strategy.profit_ratio() > 0.0); + + // Validate price range calculations + let price_range = strategy.best_range_to_show(pos!(1.0)).unwrap(); + assert!(!price_range.is_empty()); + assert!(price_range[0] < strategy.get_break_even_points()[0]); + assert!(price_range[price_range.len() - 1] > strategy.get_break_even_points()[0]); + + Ok(()) +} diff --git a/tests/unit/strategies/simple/strategy_call_butterfly.rs b/tests/unit/strategies/simple/strategy_call_butterfly.rs new file mode 100644 index 00000000..f240f2aa --- /dev/null +++ b/tests/unit/strategies/simple/strategy_call_butterfly.rs @@ -0,0 +1,72 @@ +use approx::assert_relative_eq; +use optionstratlib::constants::ZERO; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::{PositiveF64, INFINITY, PZERO}; +use optionstratlib::strategies::base::Strategies; +use optionstratlib::strategies::call_butterfly::CallButterfly; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::visualization::utils::Graph; +use optionstratlib::{assert_positivef64_relative_eq, pos}; +use std::error::Error; + +#[test] +fn test_call_butterfly_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the CallButterfly strategy + let underlying_price = pos!(5781.88); + + let strategy = CallButterfly::new( + "SP500".to_string(), + underlying_price, // underlying_price + pos!(5750.0), // long_call_strike + pos!(5800.0), // short_call_low_strike + pos!(5850.0), // short_call_high_strike + ExpirationDate::Days(2.0), + 0.18, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(1.0), // long quantity + 85.04, // premium_long_itm + 53.04, // premium_long_otm + 28.85, // premium_short + 0.78, // premium_short + 0.78, // open_fee_long + 0.78, // close_fee_long + 0.73, // close_fee_short + 0.73, // close_fee_short + 0.73, // open_fee_short + ); + + // Assertions to validate strategy properties and computations + assert_eq!(strategy.title(), "Ratio Call Spread Strategy: CallButterfly\n\tUnderlying: SP500 @ $5750 Long Call European Option\n\tUnderlying: SP500 @ $5800 Short Call European Option\n\tUnderlying: SP500 @ $5850 Short Call European Option"); + assert_eq!(strategy.get_break_even_points().len(), 2); + assert_relative_eq!(strategy.net_premium_received(), ZERO, epsilon = 0.001); + assert!(strategy.max_profit().is_ok()); + assert!(strategy.max_loss().is_ok()); + assert_positivef64_relative_eq!(strategy.max_profit()?, pos!(7.68), pos!(0.0001)); + assert_eq!(strategy.max_loss()?, INFINITY); + assert_positivef64_relative_eq!(strategy.total_cost(), pos!(89.57), pos!(0.0001)); + assert_eq!(strategy.fees(), 4.53); + + // Test range calculations + let price_range = strategy.best_range_to_show(pos!(1.0)).unwrap(); + assert!(!price_range.is_empty()); + let range = strategy.range_of_profit().unwrap(); + assert_relative_eq!(range.value(), 134.639, epsilon = 0.001); + assert_relative_eq!( + (range.value() / 2.0) / underlying_price * 100.0, + 1.164, + epsilon = 0.001 + ); + + assert!(strategy.profit_area() > 0.0); + assert!(strategy.profit_ratio() > 0.0); + + // Validate price range in relation to break even points + let break_even_points = strategy.get_break_even_points(); + assert!(price_range[0] < break_even_points[0]); + assert!(price_range[price_range.len() - 1] > break_even_points[1]); + + Ok(()) +} diff --git a/tests/unit/strategies/simple/strategy_custom.rs b/tests/unit/strategies/simple/strategy_custom.rs new file mode 100644 index 00000000..f70bfdd9 --- /dev/null +++ b/tests/unit/strategies/simple/strategy_custom.rs @@ -0,0 +1,159 @@ +use approx::assert_relative_eq; +use chrono::Utc; +use optionstratlib::model::option::Options; +use optionstratlib::model::position::Position; +use optionstratlib::model::types::{ExpirationDate, OptionStyle, OptionType, PositiveF64, Side}; +use optionstratlib::pos; +use optionstratlib::strategies::base::Strategies; +use optionstratlib::strategies::custom::CustomStrategy; +use optionstratlib::utils::logger::setup_logger; +use std::error::Error; + +#[test] +fn test_custom_strategy_integration() -> Result<(), Box> { + setup_logger(); + + // Define common parameters + let underlying_price = pos!(2340.0); + let underlying_symbol = "GAS".to_string(); + let expiration = ExpirationDate::Days(6.0); + let implied_volatility = 0.73; + let risk_free_rate = 0.05; + let dividend_yield = 0.0; + + // Create positions + let positions = vec![ + Position::new( + Options::new( + OptionType::European, + Side::Short, + underlying_symbol.clone(), + pos!(2100.0), + expiration.clone(), + implied_volatility, + pos!(2.0), + underlying_price, + risk_free_rate, + OptionStyle::Call, + dividend_yield, + None, + ), + 192.0, + Utc::now(), + 7.51, + 7.51, + ), + Position::new( + Options::new( + OptionType::European, + Side::Short, + underlying_symbol.clone(), + pos!(2250.0), + expiration.clone(), + implied_volatility, + pos!(2.0), + underlying_price, + risk_free_rate, + OptionStyle::Call, + dividend_yield, + None, + ), + 88.0, + Utc::now(), + 6.68, + 6.68, + ), + Position::new( + Options::new( + OptionType::European, + Side::Short, + underlying_symbol.clone(), + pos!(2500.0), + expiration.clone(), + implied_volatility, + pos!(1.0), + underlying_price, + risk_free_rate, + OptionStyle::Put, + dividend_yield, + None, + ), + 55.0, + Utc::now(), + 6.68, + 6.68, + ), + Position::new( + Options::new( + OptionType::European, + Side::Short, + underlying_symbol.clone(), + pos!(2150.0), + expiration.clone(), + implied_volatility, + pos!(2.5), + underlying_price, + risk_free_rate, + OptionStyle::Put, + dividend_yield, + None, + ), + 21.0, + Utc::now(), + 4.91, + 4.91, + ), + ]; + + let strategy = CustomStrategy::new( + "Custom Strategy".to_string(), + underlying_symbol, + "Example of a custom strategy".to_string(), + underlying_price, + positions, + 0.01, + 100, + 0.1, + ); + + // Test strategy properties and calculations + assert_relative_eq!(strategy.net_premium_received(), 572.83, epsilon = 0.001); + assert_relative_eq!(strategy.fees(), 51.56, epsilon = 0.001); + + // Test range and break-even points + let price_range = strategy.best_range_to_show(pos!(1.0)).unwrap(); + assert!(!price_range.is_empty()); + + // Test profit metrics + assert!( + strategy.profit_area() > 0.0 && strategy.profit_area() <= 100.0, + "Profit area should be between 0 and 100%" + ); + assert!( + strategy.profit_ratio() > 0.0, + "Profit ratio should be positive" + ); + + // Test positions + assert_eq!( + strategy.positions.len(), + 4, + "Strategy should have exactly 4 positions" + ); + + // Validate position types + let calls = strategy + .positions + .iter() + .filter(|p| matches!(p.option.option_style, OptionStyle::Call)) + .count(); + let puts = strategy + .positions + .iter() + .filter(|p| matches!(p.option.option_style, OptionStyle::Put)) + .count(); + assert_eq!(calls, 2, "Strategy should have 2 calls"); + assert_eq!(puts, 2, "Strategy should have 2 puts"); + + Ok(()) +} diff --git a/tests/unit/strategies/simple/strategy_graph.rs b/tests/unit/strategies/simple/strategy_graph.rs new file mode 100644 index 00000000..3e174010 --- /dev/null +++ b/tests/unit/strategies/simple/strategy_graph.rs @@ -0,0 +1,67 @@ +use approx::assert_relative_eq; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::{PositiveF64, PZERO}; +use optionstratlib::strategies::base::Strategies; +use optionstratlib::strategies::bull_call_spread::BullCallSpread; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::visualization::utils::Graph; +use optionstratlib::{assert_positivef64_relative_eq, pos}; +use std::error::Error; + +#[test] +fn test_bull_call_spread_basic_integration() -> Result<(), Box> { + setup_logger(); + + let strategy = BullCallSpread::new( + "GOLD".to_string(), + pos!(2505.8), // underlying_price + pos!(2460.0), // long_strike_itm + pos!(2515.0), // short_strike + ExpirationDate::Days(30.0), + 0.2, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(1.0), // quantity + 27.26, // premium_long + 5.33, // premium_short + 0.58, // open_fee_long + 0.58, // close_fee_long + 0.55, // close_fee_short + 0.54, // open_fee_short + ); + + // Validate strategy properties + assert_eq!(strategy.title(), "Bull Call Spread Strategy:\n\tUnderlying: GOLD @ $2460 Long Call European Option\n\tUnderlying: GOLD @ $2515 Short Call European Option"); + assert_eq!(strategy.get_break_even_points().len(), 1); + + // Validate financial calculations + assert_relative_eq!(strategy.net_premium_received(), -24.18, epsilon = 0.001); + assert!(strategy.max_profit().is_ok()); + assert!(strategy.max_loss().is_ok()); + assert_positivef64_relative_eq!(strategy.max_profit()?, pos!(30.82), pos!(0.0001)); + assert_positivef64_relative_eq!(strategy.max_loss()?, pos!(24.18), pos!(0.0001)); + assert_positivef64_relative_eq!(strategy.total_cost(), pos!(32.66), pos!(0.0001)); + assert_eq!(strategy.fees(), 2.25); + + // Test price range calculations + let test_price_range: Vec = (2400..2600) + .map(|x| PositiveF64::new(x as f64).unwrap()) + .collect(); + assert!(!test_price_range.is_empty()); + assert_eq!(test_price_range.len(), 200); + + // Validate strike prices relationship + assert!( + pos!(2460.0) < pos!(2515.0), + "Long strike should be less than short strike in a bull call spread" + ); + + // Validate break-even point + let break_even = strategy.break_even(); + assert!( + break_even[0] > pos!(2460.0), + "Break-even should be between strikes" + ); + + Ok(()) +} diff --git a/tests/unit/strategies/simple/strategy_iron_butterfly.rs b/tests/unit/strategies/simple/strategy_iron_butterfly.rs new file mode 100644 index 00000000..eee100a2 --- /dev/null +++ b/tests/unit/strategies/simple/strategy_iron_butterfly.rs @@ -0,0 +1,72 @@ +use approx::assert_relative_eq; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::{PositiveF64, PZERO}; +use optionstratlib::strategies::base::{Strategies, Validable}; +use optionstratlib::strategies::iron_butterfly::IronButterfly; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::{assert_positivef64_relative_eq, pos}; +use std::error::Error; + +#[test] +fn test_iron_butterfly_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the IronButterfly strategy + let underlying_price = pos!(2646.9); + + let strategy = IronButterfly::new( + "GOLD".to_string(), + underlying_price, // underlying_price + pos!(2725.0), // short_call_strike + pos!(2800.0), // long_call_strike + pos!(2500.0), // long_put_strike + ExpirationDate::Days(30.0), + 0.1548, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(2.0), // quantity + 38.8, // premium_short_call + 30.4, // premium_short_put + 23.3, // premium_long_call + 16.8, // premium_long_put + 0.96, // open_fee + 0.96, // close_fee + ); + + // Validate strategy + assert!(strategy.validate(), "Strategy should be valid"); + + // Assertions to validate strategy properties and computations + assert_eq!(strategy.get_break_even_points().len(), 2); + assert_relative_eq!(strategy.net_premium_received(), 42.839, epsilon = 0.001); + assert!(strategy.max_profit().is_ok()); + assert!(strategy.max_loss().is_ok()); + assert_positivef64_relative_eq!(strategy.max_profit()?, pos!(42.839), pos!(0.0001)); + assert_positivef64_relative_eq!(strategy.total_cost(), pos!(218.599), pos!(0.0001)); + assert_eq!(strategy.fees(), 7.68); + + // Test range calculations + let price_range = strategy.best_range_to_show(pos!(1.0)).unwrap(); + assert!(!price_range.is_empty()); + let break_even_points = strategy.get_break_even_points(); + let range = break_even_points[1] - break_even_points[0]; + assert_relative_eq!( + (range.value() / 2.0) / underlying_price.value() * 100.0, + 0.809, + epsilon = 0.001 + ); + + assert_eq!( + price_range[..4], + vec![2443.924, 2444.924, 2445.924, 2446.924] + ); + assert_relative_eq!(range.value(), 42.84, epsilon = 0.001); + + assert!(strategy.profit_area() > 0.0); + + // Validate price range in relation to break even points + assert!(price_range[0] < break_even_points[0]); + assert!(price_range[price_range.len() - 1] > break_even_points[1]); + + Ok(()) +} diff --git a/tests/unit/strategies/simple/strategy_iron_condor.rs b/tests/unit/strategies/simple/strategy_iron_condor.rs new file mode 100644 index 00000000..4283793c --- /dev/null +++ b/tests/unit/strategies/simple/strategy_iron_condor.rs @@ -0,0 +1,67 @@ +use approx::assert_relative_eq; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::{PositiveF64, PZERO}; +use optionstratlib::strategies::base::{Strategies, Validable}; +use optionstratlib::strategies::iron_condor::IronCondor; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::{assert_positivef64_relative_eq, pos}; +use std::error::Error; + +#[test] +fn test_iron_condor_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the IronCondor strategy + let underlying_price = pos!(2646.9); + + let strategy = IronCondor::new( + "GOLD".to_string(), + underlying_price, // underlying_price + pos!(2725.0), // short_call_strike + pos!(2560.0), // short_put_strike + pos!(2800.0), // long_call_strike + pos!(2500.0), // long_put_strike + ExpirationDate::Days(30.0), + 0.1548, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(2.0), // quantity + 38.8, // premium_short_call + 30.4, // premium_short_put + 23.3, // premium_long_call + 16.8, // premium_long_put + 0.96, // open_fee + 0.96, // close_fee + ); + + // Validate strategy + assert!(strategy.validate(), "Strategy should be valid"); + + // Assertions to validate strategy properties and computations + assert_eq!(strategy.get_break_even_points().len(), 2); + assert_relative_eq!(strategy.net_premium_received(), 42.839, epsilon = 0.001); + assert!(strategy.max_profit().is_ok()); + assert!(strategy.max_loss().is_ok()); + assert_positivef64_relative_eq!(strategy.max_profit()?, pos!(42.839), pos!(0.0001)); + assert_positivef64_relative_eq!(strategy.total_cost(), pos!(218.5999), pos!(0.0001)); + assert_eq!(strategy.fees(), 7.68); + + // Test range calculations + let price_range = strategy.best_range_to_show(pos!(1.0)).unwrap(); + assert!(!price_range.is_empty()); + let break_even_points = strategy.get_break_even_points(); + let range = break_even_points[1] - break_even_points[0]; + assert_relative_eq!( + (range.value() / 2.0) / underlying_price.value() * 100.0, + 3.926, + epsilon = 0.001 + ); + + assert!(strategy.profit_area() > 0.0); + + // Validate price range in relation to break even points + assert!(price_range[0] < break_even_points[0]); + assert!(price_range[price_range.len() - 1] > break_even_points[1]); + + Ok(()) +} diff --git a/tests/unit/strategies/simple/strategy_long_butterfly_spread.rs b/tests/unit/strategies/simple/strategy_long_butterfly_spread.rs new file mode 100644 index 00000000..a8439a07 --- /dev/null +++ b/tests/unit/strategies/simple/strategy_long_butterfly_spread.rs @@ -0,0 +1,56 @@ +use approx::assert_relative_eq; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::{PositiveF64, PZERO}; +use optionstratlib::strategies::base::Strategies; +use optionstratlib::strategies::butterfly_spread::LongButterflySpread; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::{assert_positivef64_relative_eq, pos}; +use std::error::Error; + +#[test] +fn test_long_butterfly_spread_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the LongButterflySpread strategy + let underlying_price = pos!(5795.88); + + let strategy = LongButterflySpread::new( + "SP500".to_string(), + underlying_price, // underlying_price + pos!(5710.0), // long_strike_itm + pos!(5780.0), // short_strike + pos!(5850.0), // long_strike_otm + ExpirationDate::Days(2.0), + 0.18, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(1.0), // long quantity + 113.30, // premium_long_low + 64.20, // premium_short + 31.65, // premium_long_high + 0.07, // fees + ); + + // Assertions to validate strategy properties and computations + assert_eq!(strategy.get_break_even_points().len(), 2); + assert_relative_eq!(strategy.net_premium_received(), -16.736, epsilon = 0.001); + assert!(strategy.max_profit().is_ok()); + assert!(strategy.max_loss().is_ok()); + assert_positivef64_relative_eq!(strategy.max_profit()?, pos!(53.263), pos!(0.0001)); + assert_positivef64_relative_eq!(strategy.max_loss()?, pos!(16.7366), pos!(0.0001)); + assert_positivef64_relative_eq!(strategy.total_cost(), pos!(273.3499), pos!(0.0001)); + assert_eq!(strategy.fees(), 0.14); + assert!(strategy.profit_area() > 0.0); + assert!(strategy.profit_ratio() > 0.0); + + // Test range calculations + let price_range = strategy.best_range_to_show(pos!(1.0)).unwrap(); + assert!(!price_range.is_empty()); + + // Validate price range in relation to break even points + let break_even_points = strategy.get_break_even_points(); + assert!(price_range[0] < break_even_points[0]); + assert!(price_range[price_range.len() - 1] > break_even_points[1]); + + Ok(()) +} diff --git a/tests/unit/strategies/simple/strategy_long_straddle.rs b/tests/unit/strategies/simple/strategy_long_straddle.rs new file mode 100644 index 00000000..f48b372a --- /dev/null +++ b/tests/unit/strategies/simple/strategy_long_straddle.rs @@ -0,0 +1,74 @@ +use approx::assert_relative_eq; +use optionstratlib::constants::ZERO; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::{PositiveF64, PZERO}; +use optionstratlib::strategies::base::Strategies; +use optionstratlib::strategies::straddle::LongStraddle; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::visualization::utils::Graph; +use optionstratlib::{assert_positivef64_relative_eq, pos}; +use std::error::Error; + +#[test] +fn test_long_straddle_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the LongStraddle strategy + let underlying_price = pos!(7008.5); + + let strategy = LongStraddle::new( + "CL".to_string(), + underlying_price, // underlying_price + pos!(7140.0), // put_strike + ExpirationDate::Days(45.0), + 0.3745, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(1.0), // quantity + 84.2, // premium_short_call + 353.2, // premium_short_put + 7.0, // open_fee_short_call + 7.01, // close_fee_short_call + 7.01, // open_fee_short_put + 7.01, // close_fee_short_put + ); + + // Assertions to validate strategy properties and computations + assert_eq!(strategy.title(), "Long Straddle Strategy: \n\tUnderlying: CL @ $7140 Long Call European Option\n\tUnderlying: CL @ $7140 Long Put European Option"); + assert_eq!(strategy.get_break_even_points().len(), 2); + assert_relative_eq!(strategy.net_premium_received(), ZERO, epsilon = 0.001); + assert!(strategy.max_profit().is_ok()); + assert!(strategy.max_loss().is_ok()); + assert_positivef64_relative_eq!(strategy.max_loss()?, pos!(465.429), pos!(0.0001)); + assert_positivef64_relative_eq!(strategy.total_cost(), pos!(465.4299), pos!(0.0001)); + assert_eq!(strategy.fees(), 28.03); + + // Test range calculations + let price_range = strategy.best_range_to_show(pos!(1.0)).unwrap(); + assert!(!price_range.is_empty()); + let break_even_points = strategy.get_break_even_points(); + let range = break_even_points[1] - break_even_points[0]; + assert_relative_eq!( + (range.value() / 2.0) / underlying_price.value() * 100.0, + 6.6409, + epsilon = 0.001 + ); + + assert!(strategy.profit_area() > 0.0); + assert!(strategy.profit_ratio() > 0.0); + + // Validate price range in relation to break even points + assert!(price_range[0] < break_even_points[0]); + assert!(price_range[price_range.len() - 1] > break_even_points[1]); + + // Additional strategy-specific validations + assert!( + strategy.get_break_even_points()[0] < strategy.get_break_even_points()[1], + "Lower break-even point should be less than upper break-even point" + ); + + // Validate that max loss is equal to net premium paid (characteristic of Long Straddle) + assert_relative_eq!(strategy.max_loss()?.value(), 465.4299, epsilon = 0.001); + + Ok(()) +} diff --git a/tests/unit/strategies/simple/strategy_long_strangle.rs b/tests/unit/strategies/simple/strategy_long_strangle.rs new file mode 100644 index 00000000..3183b28d --- /dev/null +++ b/tests/unit/strategies/simple/strategy_long_strangle.rs @@ -0,0 +1,78 @@ +use approx::assert_relative_eq; +use optionstratlib::constants::ZERO; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::{PositiveF64, PZERO}; +use optionstratlib::strategies::base::Strategies; +use optionstratlib::strategies::strangle::LongStrangle; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::visualization::utils::Graph; +use optionstratlib::{assert_positivef64_relative_eq, pos}; +use std::error::Error; + +#[test] +fn test_long_strangle_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the LongStrangle strategy + let underlying_price = pos!(7138.5); + + let strategy = LongStrangle::new( + "CL".to_string(), + underlying_price, // underlying_price + pos!(7450.0), // call_strike + pos!(7050.0), // put_strike + ExpirationDate::Days(45.0), + 0.3745, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(1.0), // quantity + 84.2, // premium_short_call + 353.2, // premium_short_put + 7.0, // open_fee_short_call + 7.01, // close_fee_short_call + 7.01, // open_fee_short_put + 7.01, // close_fee_short_put + ); + + // Assertions to validate strategy properties and computations + assert_eq!(strategy.title(), "Long Strangle Strategy: \n\tUnderlying: CL @ $7450 Long Call European Option\n\tUnderlying: CL @ $7050 Long Put European Option"); + assert_eq!(strategy.get_break_even_points().len(), 2); + assert_relative_eq!(strategy.net_premium_received(), ZERO, epsilon = 0.001); + assert!(strategy.max_profit().is_ok()); + assert!(strategy.max_loss().is_ok()); + assert_positivef64_relative_eq!(strategy.max_loss()?, pos!(465.4299), pos!(0.0001)); + assert_positivef64_relative_eq!(strategy.total_cost(), pos!(465.4299), pos!(0.0001)); + assert_eq!(strategy.fees(), 28.03); + + // Test range calculations + let price_range = strategy.best_range_to_show(pos!(1.0)).unwrap(); + assert!(!price_range.is_empty()); + let break_even_points = strategy.get_break_even_points(); + let range = break_even_points[1] - break_even_points[0]; + assert_relative_eq!( + (range.value() / 2.0) / underlying_price.value() * 100.0, + 9.3217, + epsilon = 0.001 + ); + + assert!(strategy.profit_area() > 0.0); + assert!(strategy.profit_ratio() > 0.0); + + // Validate price range in relation to break even points + assert!(price_range[0] < break_even_points[0]); + assert!(price_range[price_range.len() - 1] > break_even_points[1]); + + // Additional strategy-specific validations + assert!( + strategy.get_break_even_points()[0] < strategy.get_break_even_points()[1], + "Lower break-even point should be less than upper break-even point" + ); + + // Validate strike prices relationship (characteristic of Long Strangle) + assert!( + pos!(7050.0) < pos!(7450.0), + "Put strike should be less than call strike in a strangle" + ); + + Ok(()) +} diff --git a/tests/unit/strategies/simple/strategy_poor_mans_covered_call.rs b/tests/unit/strategies/simple/strategy_poor_mans_covered_call.rs new file mode 100644 index 00000000..d36d9e5e --- /dev/null +++ b/tests/unit/strategies/simple/strategy_poor_mans_covered_call.rs @@ -0,0 +1,60 @@ +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::{PositiveF64, PZERO}; +use optionstratlib::strategies::base::Strategies; +use optionstratlib::strategies::poor_mans_covered_call::PoorMansCoveredCall; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::{assert_positivef64_relative_eq, pos}; +use std::error::Error; + +#[test] +fn test_poor_mans_covered_call_integration() -> Result<(), Box> { + setup_logger(); + + let underlying_price = pos!(2703.3); + + let strategy = PoorMansCoveredCall::new( + "GOLD".to_string(), // underlying_symbol + underlying_price, // underlying_price + pos!(2600.0), // long_call_strike + pos!(2800.0), // short_call_strike OTM + ExpirationDate::Days(120.0), // long_call_expiration + ExpirationDate::Days(30.0), // short_call_expiration 30-45 days delta 0.30 or less + 0.17, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(2.0), // quantity + 154.7, // premium_short_call + 30.8, // premium_short_put + 1.74, // open_fee_short_call + 1.74, // close_fee_short_call + 0.85, // open_fee_short_put + 0.85, // close_fee_short_put + ); + + // Assertions to validate strategy properties and computations + assert_eq!(strategy.get_break_even_points().len(), 1); + assert!(strategy.max_profit().is_ok()); + assert!(strategy.max_loss().is_ok()); + assert_positivef64_relative_eq!(strategy.max_profit()?, pos!(141.8399), pos!(0.0001)); + assert_positivef64_relative_eq!(strategy.max_loss()?, pos!(258.16), pos!(0.0001)); + assert_eq!(strategy.fees(), 10.36); + assert!(strategy.profit_area() > 0.0); + assert!(strategy.profit_ratio() > 0.0); + + // Test range calculations + let price_range = strategy.best_range_to_show(pos!(1.0)).unwrap(); + assert!(!price_range.is_empty()); + + // Validate price range in relation to break even points + let break_even_points = strategy.get_break_even_points(); + assert!(price_range[0] < break_even_points[0]); + assert!(price_range[price_range.len() - 1] > break_even_points[0]); + + // Additional strategy-specific validations + assert!( + pos!(2600.0) < pos!(2800.0), + "Long call strike should be less than short call strike in a PMCC" + ); + + Ok(()) +} diff --git a/tests/unit/strategies/simple/strategy_short_butterfly_spread.rs b/tests/unit/strategies/simple/strategy_short_butterfly_spread.rs new file mode 100644 index 00000000..fe440b17 --- /dev/null +++ b/tests/unit/strategies/simple/strategy_short_butterfly_spread.rs @@ -0,0 +1,66 @@ +use approx::assert_relative_eq; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::{PositiveF64, PZERO}; +use optionstratlib::strategies::base::Strategies; +use optionstratlib::strategies::butterfly_spread::ShortButterflySpread; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::{assert_positivef64_relative_eq, pos}; +use std::error::Error; + +#[test] +fn test_short_butterfly_spread_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the ShortButterflySpread strategy + let underlying_price = pos!(5781.88); + + let strategy = ShortButterflySpread::new( + "SP500".to_string(), + underlying_price, // underlying_price + pos!(5700.0), // short_strike_itm + pos!(5780.0), // long_strike + pos!(5850.0), // short_strike_otm + ExpirationDate::Days(2.0), + 0.18, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(3.0), // long quantity + 119.01, // premium_long + 66.0, // premium_short + 29.85, // open_fee_long + 4.0, // open_fee_long + ); + + // Assertions to validate strategy properties and computations + assert_eq!(strategy.get_break_even_points().len(), 1); + assert_relative_eq!(strategy.net_premium_received(), 18.580000, epsilon = 0.001); + assert!(strategy.max_profit().is_ok()); + assert!(strategy.max_loss().is_ok()); + assert_positivef64_relative_eq!(strategy.max_profit()?, pos!(18.58), pos!(0.0001)); + assert_positivef64_relative_eq!(strategy.max_loss()?, pos!(221.4199), pos!(0.0001)); + assert_relative_eq!(strategy.fees(), 23.9999, epsilon = 0.001); + assert!(strategy.profit_area() > 0.0); + assert!(strategy.profit_ratio() > 0.0); + + // Test range calculations + let price_range = strategy.best_range_to_show(pos!(1.0)).unwrap(); + assert!(!price_range.is_empty()); + + // Validate price range in relation to break even points + let break_even_points = strategy.get_break_even_points(); + assert!(price_range[0] < break_even_points[0]); + + // Additional strategy-specific validations + assert!( + pos!(5700.0) < pos!(5780.0) && pos!(5780.0) < pos!(5850.0), + "Strikes should be in ascending order: short ITM < long < short OTM" + ); + + // Verify butterfly spread width is symmetrical + let width_lower = pos!(5780.0) - pos!(5700.0); + let width_upper = pos!(5850.0) - pos!(5780.0); + assert_relative_eq!(width_lower.value(), 80.0, epsilon = 0.001); + assert_relative_eq!(width_upper.value(), 70.0, epsilon = 0.001); + + Ok(()) +} diff --git a/tests/unit/strategies/simple/strategy_short_straddle.rs b/tests/unit/strategies/simple/strategy_short_straddle.rs new file mode 100644 index 00000000..c81d1df1 --- /dev/null +++ b/tests/unit/strategies/simple/strategy_short_straddle.rs @@ -0,0 +1,74 @@ +use approx::assert_relative_eq; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::{PositiveF64, PZERO}; +use optionstratlib::strategies::base::Strategies; +use optionstratlib::strategies::straddle::ShortStraddle; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::{assert_positivef64_relative_eq, pos}; +use std::error::Error; + +#[test] +fn test_short_straddle_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the ShortStraddle strategy + let underlying_price = pos!(7138.5); + + let strategy = ShortStraddle::new( + "CL".to_string(), + underlying_price, // underlying_price + pos!(7140.0), // put_strike + ExpirationDate::Days(45.0), + 0.3745, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(1.0), // quantity + 84.2, // premium_short_call + 353.2, // premium_short_put + 7.01, // open_fee_short_call + 7.01, // close_fee_short_call + 7.01, // open_fee_short_put + 7.01, // close_fee_short_put + ); + + // Assertions to validate strategy properties and computations + assert_eq!(strategy.get_break_even_points().len(), 2); + assert_relative_eq!(strategy.net_premium_received(), 409.36, epsilon = 0.001); + assert!(strategy.max_profit().is_ok()); + assert!(strategy.max_loss().is_ok()); + assert_positivef64_relative_eq!(strategy.max_profit()?, pos!(409.36), pos!(0.0001)); + assert_eq!(strategy.fees(), 28.04); + + // Test range calculations + let price_range = strategy.best_range_to_show(pos!(1.0)).unwrap(); + assert!(!price_range.is_empty()); + let break_even_points = strategy.get_break_even_points(); + let range = break_even_points[1] - break_even_points[0]; + assert_relative_eq!( + (range.value() / 2.0) / underlying_price.value() * 100.0, + 5.7345, + epsilon = 0.001 + ); + + assert!(strategy.profit_area() > 0.0); + assert!(strategy.profit_ratio() > 0.0); + + // Validate price range in relation to break even points + assert!(price_range[0] < break_even_points[0]); + assert!(price_range[price_range.len() - 1] > break_even_points[1]); + + // Additional strategy-specific validations + assert!( + break_even_points[0] < break_even_points[1], + "Lower break-even point should be less than upper break-even point" + ); + + // Validate that max profit equals net premium received (characteristic of Short Straddle) + assert_relative_eq!( + strategy.max_profit()?.value(), + strategy.net_premium_received(), + epsilon = 0.001 + ); + + Ok(()) +} diff --git a/tests/unit/strategies/simple/strategy_short_strangle.rs b/tests/unit/strategies/simple/strategy_short_strangle.rs new file mode 100644 index 00000000..b5c3c578 --- /dev/null +++ b/tests/unit/strategies/simple/strategy_short_strangle.rs @@ -0,0 +1,68 @@ +use approx::assert_relative_eq; +use optionstratlib::model::types::ExpirationDate; +use optionstratlib::model::types::{PositiveF64, PZERO}; +use optionstratlib::strategies::base::Strategies; +use optionstratlib::strategies::strangle::ShortStrangle; +use optionstratlib::utils::logger::setup_logger; +use optionstratlib::{assert_positivef64_relative_eq, pos}; +use std::error::Error; + +#[test] +fn test_short_strangle_with_greeks_integration() -> Result<(), Box> { + setup_logger(); + + // Define inputs for the ShortStrangle strategy + let underlying_price = pos!(7138.5); + + let strategy = ShortStrangle::new( + "CL".to_string(), + underlying_price, // underlying_price + pos!(7450.0), // call_strike + pos!(7050.0), // put_strike + ExpirationDate::Days(45.0), + 0.3745, // implied_volatility + 0.05, // risk_free_rate + 0.0, // dividend_yield + pos!(1.0), // quantity + 84.2, // premium_short_call + 353.2, // premium_short_put + 7.01, // open_fee_short_call + 7.01, // close_fee_short_call + 7.01, // open_fee_short_put + 7.01, // close_fee_short_put + ); + + // Assertions to validate strategy properties and computations + assert_eq!(strategy.get_break_even_points().len(), 2); + assert_relative_eq!(strategy.net_premium_received(), 409.36, epsilon = 0.001); + assert!(strategy.max_profit().is_ok()); + assert!(strategy.max_loss().is_ok()); + assert_positivef64_relative_eq!(strategy.max_profit()?, pos!(409.36), pos!(0.0001)); + assert_eq!(strategy.fees(), 28.04); + + // Test range calculations + let price_range = strategy.best_range_to_show(pos!(1.0)).unwrap(); + assert!(!price_range.is_empty()); + let break_even_points = strategy.get_break_even_points(); + let range = break_even_points[1] - break_even_points[0]; + assert_relative_eq!( + (range.value() / 2.0) / underlying_price.value() * 100.0, + 8.53624, + epsilon = 0.001 + ); + + assert!(strategy.profit_area() > 0.0); + assert!(strategy.profit_ratio() > 0.0); + + // Validate price range in relation to break even points + assert!(price_range[0] < break_even_points[0]); + assert!(price_range[price_range.len() - 1] > break_even_points[1]); + + // Additional strategy-specific validations + assert!( + pos!(7050.0) < pos!(7450.0), + "Put strike should be less than call strike in a short strangle" + ); + + Ok(()) +}