From 8ebdbf5d6a240426559311b734669c3eb4b76420 Mon Sep 17 00:00:00 2001 From: Carlo Bortolan <106114526+carlobortolan@users.noreply.github.com> Date: Sun, 30 Mar 2025 01:14:08 +0100 Subject: [PATCH 01/10] feat(options): implement bermudan options (#53) * Implement bermudan option * Implement bermudan option * Update src/options/types/bermudan_option.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Carlo Bortolan <106114526+carlobortolan@users.noreply.github.com> * Update BermudanOption * Implement bermudan option --------- Signed-off-by: Carlo Bortolan <106114526+carlobortolan@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- src/options/models/binomial_tree.rs | 13 +++- src/options/traits/option.rs | 9 +++ src/options/types.rs | 2 + src/options/types/bermudan_option.rs | 106 +++++++++++++++++++++++++++ tests/options_pricing.rs | 47 +++++++++++- 6 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 src/options/types/bermudan_option.rs diff --git a/README.md b/README.md index 1791379..2b234d2 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Quantrs supports options pricing with various models for both vanilla and exotic | --------------------------- | --------------- | -------- | ------------ | ------------ | ------------- | ------ | | European | ✅ | ✅ | ✅ | ✅ | ⏳ | ⏳ | | American | ❌ | ❌ | ✅ | ❌ (L. Sq.) | ⏳ | ❌ | -| Bermudan | ❌ | ❌ | ⏳ | ❌ (L. Sq.) | ❌ (complex) | ❌ | +| Bermudan | ❌ | ❌ | ✅ | ❌ (L. Sq.) | ❌ (complex) | ❌ | | ¹Basket | ⏳ (∀component) | ❌ | ⏳ (approx.) | ⏳ | ❌ | ❌ | | ¹Rainbow | ✅ (∀component) | ❌ | ✅ | ✅ | ❌ | ❌ | | ²Barrier | ❌ (mod. BSM) | ❌ | ⏳ | ⏳ | ⏳ | ⏳ | diff --git a/src/options/models/binomial_tree.rs b/src/options/models/binomial_tree.rs index 4a82661..0b25dde 100644 --- a/src/options/models/binomial_tree.rs +++ b/src/options/models/binomial_tree.rs @@ -138,7 +138,13 @@ impl OptionPricing for BinomialTreeModel { let expected_value = discount_factor * (p * option_values[i + 1] + (1.0 - p) * option_values[i]); - if matches!(option.style(), OptionStyle::American) { + if matches!(option.style(), OptionStyle::American) + || matches!(option.style(), OptionStyle::Bermudan) + && option + .expiration_dates() + .unwrap() + .contains(&(step as f64 * dt)) + { let early_exercise = option.payoff(Some( option.instrument().spot() * u.powi(i as i32) * d.powi((step - i) as i32), )); @@ -149,7 +155,10 @@ impl OptionPricing for BinomialTreeModel { } } - if matches!(option.style(), OptionStyle::American) { + if matches!(option.style(), OptionStyle::American) + || matches!(option.style(), OptionStyle::Bermudan) + && option.expiration_dates().unwrap().contains(&0.0) + { option_values[0].max(option.strike() - option.instrument().spot()) // TODO: Change to max(0.0, self.payoff(Some(self.spot))) } else { option_values[0] // Return the root node value diff --git a/src/options/traits/option.rs b/src/options/traits/option.rs index 928b952..70ed3a8 100644 --- a/src/options/traits/option.rs +++ b/src/options/traits/option.rs @@ -41,6 +41,15 @@ pub trait Option: Clone + Send + Sync { /// The time horizon (in years). fn time_to_maturity(&self) -> f64; + /// Get the expiration dates of the option. + /// + /// # Returns + /// + /// The expiration dates of the option. (Only for Bermudan options) + fn expiration_dates(&self) -> std::option::Option<&Vec> { + None + } + /// Set the time horizon (in years). /// /// # Arguments diff --git a/src/options/types.rs b/src/options/types.rs index a85c4b9..d670799 100644 --- a/src/options/types.rs +++ b/src/options/types.rs @@ -6,6 +6,7 @@ pub use american_option::AmericanOption; pub use asian_option::AsianOption; +pub use bermudan_option::BermudanOption; pub use binary_option::BinaryOption; pub use european_option::EuropeanOption; pub use lookback_option::LookbackOption; @@ -13,6 +14,7 @@ pub use rainbow_option::RainbowOption; mod american_option; mod asian_option; +mod bermudan_option; mod binary_option; mod european_option; mod lookback_option; diff --git a/src/options/types/bermudan_option.rs b/src/options/types/bermudan_option.rs new file mode 100644 index 0000000..c78000c --- /dev/null +++ b/src/options/types/bermudan_option.rs @@ -0,0 +1,106 @@ +//! Module for Bermudan option type. +//! +//! A Bermuda option can be exercised early, but only on a set of specific dates before its expiration. +//! These exercise dates are often set in one-month increments. +//! Premiums for Bermuda options are typically lower than those of American options, which can be exercised any time before expiry. + +use std::any::Any; + +use super::{OptionStyle, OptionType}; +use crate::{ + log_warn, + options::{Instrument, Option}, +}; + +/// A struct representing an Bermudan option. +#[derive(Clone, Debug)] +pub struct BermudanOption { + /// The underlying instrument. + pub instrument: Instrument, + /// Strike price of the option (aka exercise price). + pub strike: f64, + /// The time horizon (in years). + pub time_to_maturity: f64, + /// The expiration dates of the option (in years). + pub expiration_dates: Vec, + /// Type of the option (Call or Put). + pub option_type: OptionType, +} + +impl BermudanOption { + /// Create a new `BermudanOption`. + pub fn new( + instrument: Instrument, + strike: f64, + expiration_dates: Vec, + option_type: OptionType, + ) -> Self { + Self { + instrument, + strike, + time_to_maturity: if let Some(&last_date) = expiration_dates.last() { + last_date + } else { + log_warn!("Expiration dates are empty, setting time to maturity to 0.0"); + 0.0 + }, + expiration_dates, + option_type, + } + } +} + +impl Option for BermudanOption { + fn instrument(&self) -> &Instrument { + &self.instrument + } + + fn instrument_mut(&mut self) -> &mut Instrument { + &mut self.instrument + } + + fn set_instrument(&mut self, instrument: Instrument) { + self.instrument = instrument; + } + + fn strike(&self) -> f64 { + self.strike + } + + fn time_to_maturity(&self) -> f64 { + self.time_to_maturity + } + + fn expiration_dates(&self) -> std::option::Option<&Vec> { + Some(&self.expiration_dates) + } + + fn set_time_to_maturity(&mut self, time_to_maturity: f64) { + self.time_to_maturity = time_to_maturity; + } + + fn option_type(&self) -> OptionType { + self.option_type + } + + fn style(&self) -> &OptionStyle { + &OptionStyle::Bermudan + } + + fn flip(&self) -> Self { + let flipped_option_type = match self.option_type { + OptionType::Call => OptionType::Put, + OptionType::Put => OptionType::Call, + }; + BermudanOption::new( + self.instrument.clone(), + self.strike, + self.expiration_dates.clone(), + flipped_option_type, + ) + } + + fn as_any(&self) -> &dyn Any { + self + } +} diff --git a/tests/options_pricing.rs b/tests/options_pricing.rs index b117a8b..c4be08f 100644 --- a/tests/options_pricing.rs +++ b/tests/options_pricing.rs @@ -1,8 +1,8 @@ use approx::assert_abs_diff_eq; use quantrs::options::{ - AmericanOption, AsianOption, BinaryOption, BinomialTreeModel, Black76Model, BlackScholesModel, - EuropeanOption, Greeks, Instrument, LookbackOption, MonteCarloModel, Option, OptionGreeks, - OptionPricing, OptionType, RainbowOption, + AmericanOption, AsianOption, BermudanOption, BinaryOption, BinomialTreeModel, Black76Model, + BlackScholesModel, EuropeanOption, Greeks, Instrument, LookbackOption, MonteCarloModel, Option, + OptionGreeks, OptionPricing, OptionType, RainbowOption, }; struct MockModel {} @@ -26,6 +26,7 @@ fn assert_implements_option_trait(option: &T) { option.clone().set_time_to_maturity(1.0); option.strike(); option.time_to_maturity(); + option.expiration_dates(); option.option_type(); option.style(); option.flip(); @@ -786,6 +787,32 @@ mod binomial_tree_tests { } } + mod bermudan_option_tests { + use super::*; + + #[test] + fn test_itm() { + let instrument = Instrument::new().with_spot(52.0); + let expiration_dates = vec![0.0, 1.0, 2.0]; + let option = BermudanOption::new(instrument, 50.0, expiration_dates, OptionType::Call); + let model = BinomialTreeModel::new(0.05, 0.182321557, 2); + + assert_abs_diff_eq!(model.price(&option), 8.8258, epsilon = 0.0001); + assert_abs_diff_eq!(model.price(&option.flip()), 2.5722, epsilon = 0.0001); + } + + #[test] + fn test_otm() { + let instrument = Instrument::new().with_spot(50.0); + let expiration_dates = vec![0.0, 1.0, 2.0]; + let option = BermudanOption::new(instrument, 60.0, expiration_dates, OptionType::Call); + let model = BinomialTreeModel::new(0.05, 0.182321557, 2); + + assert_abs_diff_eq!(model.price(&option), 10.0000, epsilon = 0.0001); + assert_abs_diff_eq!(model.price(&option.flip()), 10.0000, epsilon = 0.0001); + } + } + mod rainbow_option_tests { use super::*; @@ -1908,6 +1935,20 @@ mod option_trait_tests { OptionType::Put, ); assert_implements_option_trait(&opt); + let opt = BermudanOption::new( + Instrument::new().with_spot(100.0), + 100.0, + vec![1.0], + OptionType::Call, + ); + assert_implements_option_trait(&opt); + let opt = BermudanOption::new( + Instrument::new().with_spot(100.0), + 100.0, + vec![1.0], + OptionType::Put, + ); + assert_implements_option_trait(&opt); let opt = AsianOption::fixed( Instrument::new().with_spot(100.0), 100.0, From e2f6f667216e055671c542ede45e50818a01c9bf Mon Sep 17 00:00:00 2001 From: Carlo Bortolan <106114526+carlobortolan@users.noreply.github.com> Date: Sun, 30 Mar 2025 01:14:08 +0100 Subject: [PATCH 02/10] feat(options): implement bermudan options (#53) * Implement bermudan option * Implement bermudan option * Update src/options/types/bermudan_option.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update BermudanOption * Implement bermudan option --------- Signed-off-by: Carlo Bortolan <106114526+carlobortolan@users.noreply.github.com> --- README.md | 2 +- src/options/models/binomial_tree.rs | 13 +++- src/options/traits/option.rs | 9 +++ src/options/types.rs | 2 + src/options/types/bermudan_option.rs | 106 +++++++++++++++++++++++++++ tests/options_pricing.rs | 47 +++++++++++- 6 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 src/options/types/bermudan_option.rs diff --git a/README.md b/README.md index 1791379..2b234d2 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Quantrs supports options pricing with various models for both vanilla and exotic | --------------------------- | --------------- | -------- | ------------ | ------------ | ------------- | ------ | | European | ✅ | ✅ | ✅ | ✅ | ⏳ | ⏳ | | American | ❌ | ❌ | ✅ | ❌ (L. Sq.) | ⏳ | ❌ | -| Bermudan | ❌ | ❌ | ⏳ | ❌ (L. Sq.) | ❌ (complex) | ❌ | +| Bermudan | ❌ | ❌ | ✅ | ❌ (L. Sq.) | ❌ (complex) | ❌ | | ¹Basket | ⏳ (∀component) | ❌ | ⏳ (approx.) | ⏳ | ❌ | ❌ | | ¹Rainbow | ✅ (∀component) | ❌ | ✅ | ✅ | ❌ | ❌ | | ²Barrier | ❌ (mod. BSM) | ❌ | ⏳ | ⏳ | ⏳ | ⏳ | diff --git a/src/options/models/binomial_tree.rs b/src/options/models/binomial_tree.rs index 4a82661..0b25dde 100644 --- a/src/options/models/binomial_tree.rs +++ b/src/options/models/binomial_tree.rs @@ -138,7 +138,13 @@ impl OptionPricing for BinomialTreeModel { let expected_value = discount_factor * (p * option_values[i + 1] + (1.0 - p) * option_values[i]); - if matches!(option.style(), OptionStyle::American) { + if matches!(option.style(), OptionStyle::American) + || matches!(option.style(), OptionStyle::Bermudan) + && option + .expiration_dates() + .unwrap() + .contains(&(step as f64 * dt)) + { let early_exercise = option.payoff(Some( option.instrument().spot() * u.powi(i as i32) * d.powi((step - i) as i32), )); @@ -149,7 +155,10 @@ impl OptionPricing for BinomialTreeModel { } } - if matches!(option.style(), OptionStyle::American) { + if matches!(option.style(), OptionStyle::American) + || matches!(option.style(), OptionStyle::Bermudan) + && option.expiration_dates().unwrap().contains(&0.0) + { option_values[0].max(option.strike() - option.instrument().spot()) // TODO: Change to max(0.0, self.payoff(Some(self.spot))) } else { option_values[0] // Return the root node value diff --git a/src/options/traits/option.rs b/src/options/traits/option.rs index 928b952..70ed3a8 100644 --- a/src/options/traits/option.rs +++ b/src/options/traits/option.rs @@ -41,6 +41,15 @@ pub trait Option: Clone + Send + Sync { /// The time horizon (in years). fn time_to_maturity(&self) -> f64; + /// Get the expiration dates of the option. + /// + /// # Returns + /// + /// The expiration dates of the option. (Only for Bermudan options) + fn expiration_dates(&self) -> std::option::Option<&Vec> { + None + } + /// Set the time horizon (in years). /// /// # Arguments diff --git a/src/options/types.rs b/src/options/types.rs index a85c4b9..d670799 100644 --- a/src/options/types.rs +++ b/src/options/types.rs @@ -6,6 +6,7 @@ pub use american_option::AmericanOption; pub use asian_option::AsianOption; +pub use bermudan_option::BermudanOption; pub use binary_option::BinaryOption; pub use european_option::EuropeanOption; pub use lookback_option::LookbackOption; @@ -13,6 +14,7 @@ pub use rainbow_option::RainbowOption; mod american_option; mod asian_option; +mod bermudan_option; mod binary_option; mod european_option; mod lookback_option; diff --git a/src/options/types/bermudan_option.rs b/src/options/types/bermudan_option.rs new file mode 100644 index 0000000..c78000c --- /dev/null +++ b/src/options/types/bermudan_option.rs @@ -0,0 +1,106 @@ +//! Module for Bermudan option type. +//! +//! A Bermuda option can be exercised early, but only on a set of specific dates before its expiration. +//! These exercise dates are often set in one-month increments. +//! Premiums for Bermuda options are typically lower than those of American options, which can be exercised any time before expiry. + +use std::any::Any; + +use super::{OptionStyle, OptionType}; +use crate::{ + log_warn, + options::{Instrument, Option}, +}; + +/// A struct representing an Bermudan option. +#[derive(Clone, Debug)] +pub struct BermudanOption { + /// The underlying instrument. + pub instrument: Instrument, + /// Strike price of the option (aka exercise price). + pub strike: f64, + /// The time horizon (in years). + pub time_to_maturity: f64, + /// The expiration dates of the option (in years). + pub expiration_dates: Vec, + /// Type of the option (Call or Put). + pub option_type: OptionType, +} + +impl BermudanOption { + /// Create a new `BermudanOption`. + pub fn new( + instrument: Instrument, + strike: f64, + expiration_dates: Vec, + option_type: OptionType, + ) -> Self { + Self { + instrument, + strike, + time_to_maturity: if let Some(&last_date) = expiration_dates.last() { + last_date + } else { + log_warn!("Expiration dates are empty, setting time to maturity to 0.0"); + 0.0 + }, + expiration_dates, + option_type, + } + } +} + +impl Option for BermudanOption { + fn instrument(&self) -> &Instrument { + &self.instrument + } + + fn instrument_mut(&mut self) -> &mut Instrument { + &mut self.instrument + } + + fn set_instrument(&mut self, instrument: Instrument) { + self.instrument = instrument; + } + + fn strike(&self) -> f64 { + self.strike + } + + fn time_to_maturity(&self) -> f64 { + self.time_to_maturity + } + + fn expiration_dates(&self) -> std::option::Option<&Vec> { + Some(&self.expiration_dates) + } + + fn set_time_to_maturity(&mut self, time_to_maturity: f64) { + self.time_to_maturity = time_to_maturity; + } + + fn option_type(&self) -> OptionType { + self.option_type + } + + fn style(&self) -> &OptionStyle { + &OptionStyle::Bermudan + } + + fn flip(&self) -> Self { + let flipped_option_type = match self.option_type { + OptionType::Call => OptionType::Put, + OptionType::Put => OptionType::Call, + }; + BermudanOption::new( + self.instrument.clone(), + self.strike, + self.expiration_dates.clone(), + flipped_option_type, + ) + } + + fn as_any(&self) -> &dyn Any { + self + } +} diff --git a/tests/options_pricing.rs b/tests/options_pricing.rs index b117a8b..c4be08f 100644 --- a/tests/options_pricing.rs +++ b/tests/options_pricing.rs @@ -1,8 +1,8 @@ use approx::assert_abs_diff_eq; use quantrs::options::{ - AmericanOption, AsianOption, BinaryOption, BinomialTreeModel, Black76Model, BlackScholesModel, - EuropeanOption, Greeks, Instrument, LookbackOption, MonteCarloModel, Option, OptionGreeks, - OptionPricing, OptionType, RainbowOption, + AmericanOption, AsianOption, BermudanOption, BinaryOption, BinomialTreeModel, Black76Model, + BlackScholesModel, EuropeanOption, Greeks, Instrument, LookbackOption, MonteCarloModel, Option, + OptionGreeks, OptionPricing, OptionType, RainbowOption, }; struct MockModel {} @@ -26,6 +26,7 @@ fn assert_implements_option_trait(option: &T) { option.clone().set_time_to_maturity(1.0); option.strike(); option.time_to_maturity(); + option.expiration_dates(); option.option_type(); option.style(); option.flip(); @@ -786,6 +787,32 @@ mod binomial_tree_tests { } } + mod bermudan_option_tests { + use super::*; + + #[test] + fn test_itm() { + let instrument = Instrument::new().with_spot(52.0); + let expiration_dates = vec![0.0, 1.0, 2.0]; + let option = BermudanOption::new(instrument, 50.0, expiration_dates, OptionType::Call); + let model = BinomialTreeModel::new(0.05, 0.182321557, 2); + + assert_abs_diff_eq!(model.price(&option), 8.8258, epsilon = 0.0001); + assert_abs_diff_eq!(model.price(&option.flip()), 2.5722, epsilon = 0.0001); + } + + #[test] + fn test_otm() { + let instrument = Instrument::new().with_spot(50.0); + let expiration_dates = vec![0.0, 1.0, 2.0]; + let option = BermudanOption::new(instrument, 60.0, expiration_dates, OptionType::Call); + let model = BinomialTreeModel::new(0.05, 0.182321557, 2); + + assert_abs_diff_eq!(model.price(&option), 10.0000, epsilon = 0.0001); + assert_abs_diff_eq!(model.price(&option.flip()), 10.0000, epsilon = 0.0001); + } + } + mod rainbow_option_tests { use super::*; @@ -1908,6 +1935,20 @@ mod option_trait_tests { OptionType::Put, ); assert_implements_option_trait(&opt); + let opt = BermudanOption::new( + Instrument::new().with_spot(100.0), + 100.0, + vec![1.0], + OptionType::Call, + ); + assert_implements_option_trait(&opt); + let opt = BermudanOption::new( + Instrument::new().with_spot(100.0), + 100.0, + vec![1.0], + OptionType::Put, + ); + assert_implements_option_trait(&opt); let opt = AsianOption::fixed( Instrument::new().with_spot(100.0), 100.0, From 2ff5391b9474d2abebe1ecf9ba197013733ed755 Mon Sep 17 00:00:00 2001 From: carlobortolan Date: Sun, 30 Mar 2025 16:45:14 +0200 Subject: [PATCH 03/10] Update option style methods to return by value and add barrier option type --- src/options/traits/option.rs | 2 +- src/options/types.rs | 2 + src/options/types/american_option.rs | 6 +- src/options/types/asian_option.rs | 19 +++-- src/options/types/barrier_option.rs | 101 +++++++++++++++++++++++++++ src/options/types/bermudan_option.rs | 4 +- src/options/types/binary_option.rs | 8 +-- src/options/types/european_option.rs | 4 +- src/options/types/lookback_option.rs | 23 +++--- src/options/types/rainbow_option.rs | 30 +++----- 10 files changed, 146 insertions(+), 53 deletions(-) create mode 100644 src/options/types/barrier_option.rs diff --git a/src/options/traits/option.rs b/src/options/traits/option.rs index 70ed3a8..5be450c 100644 --- a/src/options/traits/option.rs +++ b/src/options/traits/option.rs @@ -69,7 +69,7 @@ pub trait Option: Clone + Send + Sync { /// # Returns /// /// The style of the option. - fn style(&self) -> &OptionStyle; + fn style(&self) -> OptionStyle; /// Flip the option type (Call to Put or Put to Call). /// diff --git a/src/options/types.rs b/src/options/types.rs index d670799..42e7dbc 100644 --- a/src/options/types.rs +++ b/src/options/types.rs @@ -6,6 +6,7 @@ pub use american_option::AmericanOption; pub use asian_option::AsianOption; +pub use barrier_option::BarrierOption; pub use bermudan_option::BermudanOption; pub use binary_option::BinaryOption; pub use european_option::EuropeanOption; @@ -14,6 +15,7 @@ pub use rainbow_option::RainbowOption; mod american_option; mod asian_option; +mod barrier_option; mod bermudan_option; mod binary_option; mod european_option; diff --git a/src/options/types/american_option.rs b/src/options/types/american_option.rs index fe8ebf1..7594e18 100644 --- a/src/options/types/american_option.rs +++ b/src/options/types/american_option.rs @@ -34,9 +34,9 @@ pub struct AmericanOption { pub instrument: Instrument, /// Strike price of the option (aka exercise price). pub strike: f64, - /// Type of the option (Call or Put). /// The time horizon (in years). pub time_to_maturity: f64, + /// Type of the option (Call or Put). pub option_type: OptionType, } @@ -86,8 +86,8 @@ impl Option for AmericanOption { self.option_type } - fn style(&self) -> &OptionStyle { - &OptionStyle::American + fn style(&self) -> OptionStyle { + OptionStyle::American } fn flip(&self) -> Self { diff --git a/src/options/types/asian_option.rs b/src/options/types/asian_option.rs index bcc45bd..4e375a6 100644 --- a/src/options/types/asian_option.rs +++ b/src/options/types/asian_option.rs @@ -35,10 +35,8 @@ pub struct AsianOption { pub time_to_maturity: f64, /// Type of the option (Call or Put). pub option_type: OptionType, - /// The style of the option (Asian). - pub option_style: OptionStyle, - /// The type of the Asian option (Fixed or Floating). - pub asian_type: Permutation, + /// Style of the option (Asian with specific type). + pub permutation: Permutation, } impl AsianOption { @@ -48,15 +46,14 @@ impl AsianOption { strike: f64, time_to_maturity: f64, option_type: OptionType, - asian_type: Permutation, + permutation: Permutation, ) -> Self { Self { instrument, strike, time_to_maturity, option_type, - option_style: OptionStyle::Asian(asian_type), - asian_type, + permutation, } } @@ -121,13 +118,13 @@ impl Option for AsianOption { self.option_type } - fn style(&self) -> &OptionStyle { - &self.option_style + fn style(&self) -> OptionStyle { + OptionStyle::Asian(self.permutation) } fn payoff(&self, avg_price: std::option::Option) -> f64 { let avg_price = avg_price.unwrap_or(self.instrument.spot()); - match self.asian_type { + match self.permutation { Permutation::Fixed => match self.option_type { OptionType::Call => (avg_price - self.strike).max(0.0), OptionType::Put => (self.strike - avg_price).max(0.0), @@ -149,7 +146,7 @@ impl Option for AsianOption { self.strike, self.time_to_maturity, flipped_option_type, - self.asian_type, + self.permutation, ) } diff --git a/src/options/types/barrier_option.rs b/src/options/types/barrier_option.rs new file mode 100644 index 0000000..51b6d77 --- /dev/null +++ b/src/options/types/barrier_option.rs @@ -0,0 +1,101 @@ +//! Module for Barrier option type. + +use std::any::Any; + +use super::{BarrierType, OptionStyle, OptionType}; +use crate::options::{Instrument, Option}; + +/// A struct representing an Bermudan option. +#[derive(Clone, Debug)] +pub struct BarrierOption { + /// The underlying instrument. + pub instrument: Instrument, + /// Strike price of the option (aka exercise price). + pub strike: f64, + /// The barrier price + pub barrier: f64, + /// The time horizon (in years). + pub time_to_maturity: f64, + /// Type of the option (Call or Put). + pub option_type: OptionType, + /// Style of the option (Barrier with specific type). + pub barrier_type: BarrierType, +} + +impl BarrierOption { + /// Create a new `BarrierOption`. + pub fn new( + instrument: Instrument, + strike: f64, + barrier: f64, + time_to_maturity: f64, + option_type: OptionType, + barrier_type: BarrierType, + ) -> Self { + Self { + instrument, + strike, + barrier, + time_to_maturity, + option_type, + barrier_type, + } + } +} + +impl Option for BarrierOption { + fn instrument(&self) -> &Instrument { + &self.instrument + } + + fn instrument_mut(&mut self) -> &mut Instrument { + &mut self.instrument + } + + fn set_instrument(&mut self, instrument: Instrument) { + self.instrument = instrument; + } + + fn strike(&self) -> f64 { + self.strike + } + + fn time_to_maturity(&self) -> f64 { + self.time_to_maturity + } + + fn set_time_to_maturity(&mut self, time_to_maturity: f64) { + self.time_to_maturity = time_to_maturity; + } + + fn option_type(&self) -> OptionType { + self.option_type + } + + fn style(&self) -> OptionStyle { + OptionStyle::Barrier(self.barrier_type) + } + + fn payoff(&self, avg_price: std::option::Option) -> f64 { + todo!("Implement payoff calculation for BarrierOption"); + } + + fn flip(&self) -> Self { + let flipped_option_type = match self.option_type { + OptionType::Call => OptionType::Put, + OptionType::Put => OptionType::Call, + }; + BarrierOption::new( + self.instrument.clone(), + self.strike, + self.barrier, + self.time_to_maturity, + flipped_option_type, + self.barrier_type, + ) + } + + fn as_any(&self) -> &dyn Any { + self + } +} diff --git a/src/options/types/bermudan_option.rs b/src/options/types/bermudan_option.rs index c78000c..596489a 100644 --- a/src/options/types/bermudan_option.rs +++ b/src/options/types/bermudan_option.rs @@ -83,8 +83,8 @@ impl Option for BermudanOption { self.option_type } - fn style(&self) -> &OptionStyle { - &OptionStyle::Bermudan + fn style(&self) -> OptionStyle { + OptionStyle::Bermudan } fn flip(&self) -> Self { diff --git a/src/options/types/binary_option.rs b/src/options/types/binary_option.rs index be406a2..26d4edd 100644 --- a/src/options/types/binary_option.rs +++ b/src/options/types/binary_option.rs @@ -51,14 +51,14 @@ impl BinaryOption { strike: f64, time_to_maturity: f64, option_type: OptionType, - binary_option_type: BinaryType, + binary_type: BinaryType, ) -> Self { Self { instrument, strike, time_to_maturity, option_type, - option_style: OptionStyle::Binary(binary_option_type), + option_style: OptionStyle::Binary(binary_type), } } @@ -133,8 +133,8 @@ impl Option for BinaryOption { self.option_type } - fn style(&self) -> &OptionStyle { - &self.option_style + fn style(&self) -> OptionStyle { + self.option_style } fn flip(&self) -> Self { diff --git a/src/options/types/european_option.rs b/src/options/types/european_option.rs index 5bcd2be..61bcfe8 100644 --- a/src/options/types/european_option.rs +++ b/src/options/types/european_option.rs @@ -86,8 +86,8 @@ impl Option for EuropeanOption { self.option_type } - fn style(&self) -> &OptionStyle { - &OptionStyle::European + fn style(&self) -> OptionStyle { + OptionStyle::European } fn flip(&self) -> Self { diff --git a/src/options/types/lookback_option.rs b/src/options/types/lookback_option.rs index a945868..d571128 100644 --- a/src/options/types/lookback_option.rs +++ b/src/options/types/lookback_option.rs @@ -2,12 +2,16 @@ use crate::options::{types::Permutation, Instrument, Option, OptionStyle, Option #[derive(Clone, Debug)] pub struct LookbackOption { + /// The underlying instrument. pub instrument: Instrument, - pub time_to_maturity: f64, + /// Strike price of the option (aka exercise price). pub strike: f64, + /// The time horizon (in years). + pub time_to_maturity: f64, + /// Type of the option (Call or Put). pub option_type: OptionType, - pub option_style: OptionStyle, - pub lookback_type: Permutation, + /// Style of the option (Lookback with specific type). + pub permutation: Permutation, } impl LookbackOption { @@ -17,15 +21,14 @@ impl LookbackOption { strike: f64, time_to_maturity: f64, option_type: OptionType, - lookback_type: Permutation, + permutation: Permutation, ) -> Self { Self { instrument, time_to_maturity, strike, option_type, - option_style: OptionStyle::Lookback(lookback_type), - lookback_type, + permutation, } } @@ -90,15 +93,15 @@ impl Option for LookbackOption { self.option_type } - fn style(&self) -> &OptionStyle { - &self.option_style + fn style(&self) -> OptionStyle { + OptionStyle::Lookback(self.permutation) } fn payoff(&self, avg_price: std::option::Option) -> f64 { let avg_price = avg_price.unwrap_or(self.instrument.spot()); let max_spot = self.instrument.max_spot(); - match self.lookback_type { + match self.permutation { Permutation::Fixed => match self.option_type { OptionType::Call => (self.instrument.max_spot() - self.strike).max(0.0), OptionType::Put => (self.strike - self.instrument.min_spot()).max(0.0), @@ -124,7 +127,7 @@ impl Option for LookbackOption { self.strike, self.time_to_maturity, flipped_option_type, - self.lookback_type, + self.permutation, ) } diff --git a/src/options/types/rainbow_option.rs b/src/options/types/rainbow_option.rs index a9cb1ec..8ed6ae7 100644 --- a/src/options/types/rainbow_option.rs +++ b/src/options/types/rainbow_option.rs @@ -58,7 +58,6 @@ use super::{OptionStyle, OptionType, RainbowType, RainbowType::*}; use crate::options::{Instrument, Option}; -use core::panic; use std::any::Any; /// A struct representing a Rainbow option. @@ -73,7 +72,7 @@ pub struct RainbowOption { /// Type of the option (Call or Put). pub option_type: OptionType, /// Style of the option (Rainbow with specific type). - pub option_style: OptionStyle, + pub rainbow_type: RainbowType, } impl RainbowOption { @@ -83,14 +82,14 @@ impl RainbowOption { strike: f64, time_to_maturity: f64, option_type: OptionType, - rainbow_option_type: RainbowType, + rainbow_type: RainbowType, ) -> Self { Self { instrument, strike, time_to_maturity, option_type, - option_style: OptionStyle::Rainbow(rainbow_option_type), + rainbow_type, } } @@ -149,20 +148,11 @@ impl RainbowOption { pub fn all_otm(instrument: Instrument, strike: f64, ttm: f64) -> Self { Self::new(instrument, strike, ttm, OptionType::Put, AllOTM) } - - /// Get the Rainbow option type. - pub fn rainbow_option_type(&self) -> &RainbowType { - if let OptionStyle::Rainbow(ref rainbow_option_type) = self.option_style { - rainbow_option_type - } else { - panic!("Not a rainbow option") - } - } } impl Option for RainbowOption { fn instrument(&self) -> &Instrument { - match self.rainbow_option_type() { + match self.rainbow_type { BestOf | CallOnMax | PutOnMax => self.instrument.best_performer(), WorstOf | CallOnMin | PutOnMin => self.instrument.worst_performer(), _ => &self.instrument, @@ -170,7 +160,7 @@ impl Option for RainbowOption { } fn instrument_mut(&mut self) -> &mut Instrument { - match self.rainbow_option_type() { + match self.rainbow_type { BestOf | CallOnMax | PutOnMax => self.instrument.best_performer_mut(), WorstOf | CallOnMin | PutOnMin => self.instrument.worst_performer_mut(), _ => &mut self.instrument, @@ -197,8 +187,8 @@ impl Option for RainbowOption { self.option_type } - fn style(&self) -> &OptionStyle { - &self.option_style + fn style(&self) -> OptionStyle { + OptionStyle::Rainbow(self.rainbow_type) } fn flip(&self) -> Self { @@ -207,7 +197,7 @@ impl Option for RainbowOption { OptionType::Put => OptionType::Call, }; - let flipped_option_style = match self.rainbow_option_type() { + let flipped_rainbow_type = match self.rainbow_type { BestOf => BestOf, WorstOf => WorstOf, CallOnMax => PutOnMax, @@ -225,14 +215,14 @@ impl Option for RainbowOption { self.strike, self.time_to_maturity, flipped_option_type, - flipped_option_style, + flipped_rainbow_type, ) } fn payoff(&self, spot: std::option::Option) -> f64 { let spot_price: f64 = spot.unwrap_or_else(|| self.instrument().spot()); - match self.rainbow_option_type() { + match self.rainbow_type { BestOf => spot_price.max(self.strike), WorstOf => spot_price.min(self.strike), CallOnMax => (spot_price - self.strike).max(0.0), From 73be663088475088468af919a42be353c56adefa Mon Sep 17 00:00:00 2001 From: carlobortolan Date: Sun, 30 Mar 2025 16:55:36 +0200 Subject: [PATCH 04/10] Add down-and-in, down-and-out, up-and-in, and up-and-out types --- src/options/types/barrier_option.rs | 68 +++++++++++++++++++++++++++++ tests/options_pricing.rs | 22 ++++++++-- 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/src/options/types/barrier_option.rs b/src/options/types/barrier_option.rs index 51b6d77..9eacb4e 100644 --- a/src/options/types/barrier_option.rs +++ b/src/options/types/barrier_option.rs @@ -41,6 +41,74 @@ impl BarrierOption { barrier_type, } } + + pub fn down_and_in( + instrument: Instrument, + strike: f64, + barrier: f64, + time_to_maturity: f64, + option_type: OptionType, + ) -> Self { + Self::new( + instrument, + strike, + barrier, + time_to_maturity, + option_type, + BarrierType::DownAndIn, + ) + } + + pub fn down_and_out( + instrument: Instrument, + strike: f64, + barrier: f64, + time_to_maturity: f64, + option_type: OptionType, + ) -> Self { + Self::new( + instrument, + strike, + barrier, + time_to_maturity, + option_type, + BarrierType::DownAndOut, + ) + } + + pub fn up_and_in( + instrument: Instrument, + strike: f64, + barrier: f64, + time_to_maturity: f64, + option_type: OptionType, + ) -> Self { + Self::new( + instrument, + strike, + barrier, + time_to_maturity, + option_type, + BarrierType::UpAndIn, + ) + } + + pub fn up_and_out( + instrument: Instrument, + strike: f64, + barrier: f64, + time_to_maturity: f64, + option_type: OptionType, + ) -> Self { + Self::new( + instrument, + strike, + barrier, + time_to_maturity, + option_type, + BarrierType::UpAndOut, + ) + } } impl Option for BarrierOption { diff --git a/tests/options_pricing.rs b/tests/options_pricing.rs index c4be08f..80bd486 100644 --- a/tests/options_pricing.rs +++ b/tests/options_pricing.rs @@ -1,8 +1,8 @@ use approx::assert_abs_diff_eq; use quantrs::options::{ - AmericanOption, AsianOption, BermudanOption, BinaryOption, BinomialTreeModel, Black76Model, - BlackScholesModel, EuropeanOption, Greeks, Instrument, LookbackOption, MonteCarloModel, Option, - OptionGreeks, OptionPricing, OptionType, RainbowOption, + AmericanOption, AsianOption, BarrierOption, BermudanOption, BinaryOption, BinomialTreeModel, + Black76Model, BlackScholesModel, EuropeanOption, Greeks, Instrument, LookbackOption, + MonteCarloModel, Option, OptionGreeks, OptionPricing, OptionType, RainbowOption, }; struct MockModel {} @@ -1983,6 +1983,22 @@ mod option_trait_tests { let opt = LookbackOption::floating(Instrument::new().with_spot(100.0), 1.0, OptionType::Put); assert_implements_option_trait(&opt); + let opt = BarrierOption::down_and_in( + Instrument::new().with_spot(100.0), + 100.0, + 110.0, + 1.0, + OptionType::Put, + ); + assert_implements_option_trait(&opt); + let opt = BarrierOption::down_and_in( + Instrument::new().with_spot(100.0), + 100.0, + 110.0, + 1.0, + OptionType::Put, + ); + assert_implements_option_trait(&opt); let opt = BinaryOption::cash_or_nothing( Instrument::new().with_spot(100.0), 100.0, From fd01a342b6cdecca14fea086dbfcb5c51aee21a0 Mon Sep 17 00:00:00 2001 From: carlobortolan Date: Sun, 30 Mar 2025 17:10:05 +0200 Subject: [PATCH 05/10] Implement payoff for BarrierOption --- src/options/types/barrier_option.rs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/options/types/barrier_option.rs b/src/options/types/barrier_option.rs index 9eacb4e..4d7687a 100644 --- a/src/options/types/barrier_option.rs +++ b/src/options/types/barrier_option.rs @@ -144,8 +144,34 @@ impl Option for BarrierOption { OptionStyle::Barrier(self.barrier_type) } + #[rustfmt::skip] fn payoff(&self, avg_price: std::option::Option) -> f64 { - todo!("Implement payoff calculation for BarrierOption"); + let terminal = self.instrument.terminal_spot(); + let above = self.instrument.spot.iter().any(|&a| a >= self.barrier); + let below = self.instrument.spot.iter().any(|&a| a <= self.barrier); + let payoff = match self.option_type { + OptionType::Call => (terminal - self.strike).max(0.0), + OptionType::Put => (self.strike - terminal).max(0.0), + }; + + match self.barrier_type { + BarrierType::DownAndIn => match self.option_type { + OptionType::Call => if below { payoff } else { 0.0 }, + OptionType::Put => if below { payoff } else { 0.0 }, + }, + BarrierType::DownAndOut => match self.option_type { + OptionType::Call => if below { payoff } else { 0.0 }, + OptionType::Put => if below { payoff } else { 0.0 }, + }, + BarrierType::UpAndIn => match self.option_type { + OptionType::Call => if above { 0.0 } else { payoff }, + OptionType::Put => if above { 0.0 } else { payoff }, + }, + BarrierType::UpAndOut => match self.option_type { + OptionType::Call => if above { 0.0 } else { payoff }, + OptionType::Put => if above { 0.0 } else { payoff }, + }, + } } fn flip(&self) -> Self { From cb235a5472fe6fd8492da3dbf5d96e158c52069a Mon Sep 17 00:00:00 2001 From: carlobortolan Date: Mon, 31 Mar 2025 01:01:46 +0200 Subject: [PATCH 06/10] Implement binomial for upward barrier --- README.md | 4 +- src/options/models/binomial_tree.rs | 8 +- src/options/types/barrier_option.rs | 42 +++++----- tests/options_pricing.rs | 115 ++++++++++++++++++++++++++++ 4 files changed, 147 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 2b234d2..35eef0b 100644 --- a/README.md +++ b/README.md @@ -41,12 +41,12 @@ Quantrs supports options pricing with various models for both vanilla and exotic | Bermudan | ❌ | ❌ | ✅ | ❌ (L. Sq.) | ❌ (complex) | ❌ | | ¹Basket | ⏳ (∀component) | ❌ | ⏳ (approx.) | ⏳ | ❌ | ❌ | | ¹Rainbow | ✅ (∀component) | ❌ | ✅ | ✅ | ❌ | ❌ | -| ²Barrier | ❌ (mod. BSM) | ❌ | ⏳ | ⏳ | ⏳ | ⏳ | +| ²Barrier | ❌ (mod. BSM) | ❌ | ✅ | ✅ | ⏳ | ⏳ | | ²Double Barrier | ❌ (mod. BSM) | ❌ | ⏳ | ⏳ | ❌ (complex) | ⏳ | | ²Asian (fixed strike) | ❌ (mod. BSM) | ❌ | ❌ | ✅ | ⏳ | ⏳ | | ²Asian (floating strike) | ❌ (mod. BSM) | ❌ | ❌ | ✅ | ⏳ | ⏳ | | ²Lookback (fixed strike) | ❌ | ❌ | ❌ | ✅ | ⏳ | ⏳ | -| ²Lookback (floating strike) | ✅ | ❌ | ❌ | ✅ | ⏳ | ⏳ | +| ²Lookback (floating strike) | ✅ (!spots) | ❌ | ❌ | ✅ | ⏳ | ⏳ | | ²Binary Cash-or-Nothing | ✅ | ❌ | ✅ | ✅ | ❌ (mod. PDE) | ⏳ | | ²Binary Asset-or-Nothing | ✅ | ❌ | ✅ | ✅ | ❌ (mod. PDE) | ⏳ | | Greeks (Δ,ν,Θ,ρ,Γ) | ✅ | ✅ | ⏳ | ❌ | ❌ | ❌ | diff --git a/src/options/models/binomial_tree.rs b/src/options/models/binomial_tree.rs index 0b25dde..a5ed87f 100644 --- a/src/options/models/binomial_tree.rs +++ b/src/options/models/binomial_tree.rs @@ -72,7 +72,7 @@ //! println!("Option price: {}", price); //! ``` -use crate::options::{Option, OptionPricing, OptionStrategy, OptionStyle}; +use crate::options::{BarrierOption, Option, OptionPricing, OptionStrategy, OptionStyle}; /// Binomial tree option pricing model. #[derive(Debug, Default)] @@ -135,9 +135,15 @@ impl OptionPricing for BinomialTreeModel { // Backward induction for step in (0..self.steps).rev() { for i in 0..=step { + let mut path = vec![]; + let spot = + option.instrument().spot() * u.powi(i as i32) * d.powi((step - i) as i32); + path.push(spot); + let expected_value = discount_factor * (p * option_values[i + 1] + (1.0 - p) * option_values[i]); + // Check if the option can be exercised early if matches!(option.style(), OptionStyle::American) || matches!(option.style(), OptionStyle::Bermudan) && option diff --git a/src/options/types/barrier_option.rs b/src/options/types/barrier_option.rs index 4d7687a..ad6d13b 100644 --- a/src/options/types/barrier_option.rs +++ b/src/options/types/barrier_option.rs @@ -109,6 +109,22 @@ impl BarrierOption { BarrierType::UpAndOut, ) } + + pub fn is_knocked_out(&self, spot: f64) -> bool { + match self.barrier_type { + BarrierType::DownAndOut => spot <= self.barrier, + BarrierType::UpAndOut => spot >= self.barrier, + _ => false, // In-options are never knocked out + } + } + + pub fn is_activated(&self, path: &[f64]) -> bool { + match self.barrier_type { + BarrierType::DownAndIn => path.iter().any(|&s| s <= self.barrier), + BarrierType::UpAndIn => path.iter().any(|&s| s >= self.barrier), + _ => true, // Out-options don't require activation + } + } } impl Option for BarrierOption { @@ -145,32 +161,20 @@ impl Option for BarrierOption { } #[rustfmt::skip] - fn payoff(&self, avg_price: std::option::Option) -> f64 { - let terminal = self.instrument.terminal_spot(); + fn payoff(&self, terminal: std::option::Option) -> f64 { + let terminal = terminal.unwrap_or_else(|| self.instrument.terminal_spot()); let above = self.instrument.spot.iter().any(|&a| a >= self.barrier); let below = self.instrument.spot.iter().any(|&a| a <= self.barrier); let payoff = match self.option_type { OptionType::Call => (terminal - self.strike).max(0.0), OptionType::Put => (self.strike - terminal).max(0.0), }; - + match self.barrier_type { - BarrierType::DownAndIn => match self.option_type { - OptionType::Call => if below { payoff } else { 0.0 }, - OptionType::Put => if below { payoff } else { 0.0 }, - }, - BarrierType::DownAndOut => match self.option_type { - OptionType::Call => if below { payoff } else { 0.0 }, - OptionType::Put => if below { payoff } else { 0.0 }, - }, - BarrierType::UpAndIn => match self.option_type { - OptionType::Call => if above { 0.0 } else { payoff }, - OptionType::Put => if above { 0.0 } else { payoff }, - }, - BarrierType::UpAndOut => match self.option_type { - OptionType::Call => if above { 0.0 } else { payoff }, - OptionType::Put => if above { 0.0 } else { payoff }, - }, + BarrierType::DownAndIn => if below { payoff } else { 0.0 }, + BarrierType::DownAndOut => if below { 0.0 } else { payoff }, + BarrierType::UpAndIn => if above { payoff } else { 0.0 }, + BarrierType::UpAndOut => if above { 0.0 } else { payoff }, } } diff --git a/tests/options_pricing.rs b/tests/options_pricing.rs index 80bd486..38c931d 100644 --- a/tests/options_pricing.rs +++ b/tests/options_pricing.rs @@ -740,6 +740,7 @@ mod black_scholes_tests { // Binomial Tree Model Tests mod binomial_tree_tests { use super::*; + mod european_option_tests { use super::*; #[test] @@ -1065,6 +1066,63 @@ mod binomial_tree_tests { } } + mod barrier_option_tests { + use super::*; + + #[test] + fn test_down_and_in() { + let instrument = Instrument::new().with_spot(42.0).with_continuous_dividend_yield(0.08); + let option = BarrierOption::down_and_in(instrument, 42.0, 42.0, 1.5, OptionType::Call); + let model = BinomialTreeModel::new(0.05, 0.2, 252); + + let price: f64 = model.price(&option); + assert_abs_diff_eq!(price, 2.9158, epsilon = 0.0001); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 4.6304, epsilon = 0.0001); + } + + #[test] + fn test_down_and_out() { + let instrument = Instrument::new().with_spot(42.0); + let option = + BarrierOption::down_and_out(instrument, 100.0, 100.0, 2.0, OptionType::Call); + let model = BinomialTreeModel::new(0.05, 0.2, 252); + + let price = model.price(&option); + assert_abs_diff_eq!(price, 0.0000, epsilon = 0.0001); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 0.0000, epsilon = 0.0001); + } + + #[test] + fn test_up_and_in() { + let instrument = Instrument::new().with_spot(42.0).with_continuous_dividend_yield(0.08); + let option = BarrierOption::up_and_in(instrument, 100.0, 100.0, 2.0, OptionType::Call); + let model = BinomialTreeModel::new(0.05, 0.2, 252); + + let price: f64 = model.price(&option); + assert_abs_diff_eq!(price, 2.9158, epsilon = 0.0001); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 4.6304, epsilon = 0.0001); + } + + #[test] + fn test_up_and_out() { + let instrument = Instrument::new().with_spot(42.0); + let option = BarrierOption::up_and_out(instrument, 100.0, 100.0, 2.0, OptionType::Call); + let model = BinomialTreeModel::new(0.05, 0.2, 252); + + let price = model.price(&option); + assert_abs_diff_eq!(price, 0.0000, epsilon = 0.0001); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 0.0000, epsilon = 0.0001); + } + } + #[test] fn test_binomial_tree_iv() { let instrument = Instrument::new().with_spot(100.0); @@ -1532,6 +1590,63 @@ mod monte_carlo_tests { assert_abs_diff_eq!(price, 0.0, epsilon = 2.0); } } + + mod barrier_option_tests { + use super::*; + + #[test] + fn test_down_and_in() { + let instrument = Instrument::new().with_spot(42.0); + let option = BarrierOption::down_and_in(instrument, 42.0, 42.0, 1.5, OptionType::Call); + let model = MonteCarloModel::geometric(0.05, 0.2, 4_000, 20); + + let price: f64 = model.price(&option); + assert_abs_diff_eq!(price, 5.65, epsilon = 0.0001); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 2.61, epsilon = 0.0001); + } + + #[test] + fn test_down_and_out() { + let instrument = Instrument::new().with_spot(42.0); + let option = + BarrierOption::down_and_out(instrument, 100.0, 100.0, 2.0, OptionType::Call); + let model = MonteCarloModel::geometric(0.05, 0.2, 4_000, 20); + + let price = model.price(&option); + assert_abs_diff_eq!(price, 0.0000, epsilon = 1.0); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 0.0000, epsilon = 1.0); + } + + #[test] + fn test_up_and_in() { + let instrument = Instrument::new().with_spot(42.0); + let option = BarrierOption::up_and_in(instrument, 100.0, 100.0, 2.0, OptionType::Call); + let model = MonteCarloModel::geometric(0.05, 0.2, 4_000, 20); + + let price: f64 = model.price(&option); + assert_abs_diff_eq!(price, 5.65, epsilon = 0.0001); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 2.61, epsilon = 0.0001); + } + + #[test] + fn test_up_and_out() { + let instrument = Instrument::new().with_spot(42.0); + let option = BarrierOption::up_and_out(instrument, 100.0, 100.0, 2.0, OptionType::Call); + let model = MonteCarloModel::geometric(0.05, 0.2, 4_000, 20); + + let price = model.price(&option); + assert_abs_diff_eq!(price, 0.0000, epsilon = 1.0); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 0.0000, epsilon = 1.0); + } + } } // Black-76 Model Tests From 6eacd88cf83e4b6de0e1788e57e7d9e7e62d4610 Mon Sep 17 00:00:00 2001 From: carlobortolan Date: Mon, 31 Mar 2025 02:16:52 +0200 Subject: [PATCH 07/10] Implement mc for barrier --- src/options/models/monte_carlo.rs | 54 ++++- src/options/types/barrier_option.rs | 25 ++- tests/options_pricing.rs | 326 +++++++++++++++++++++++++--- 3 files changed, 363 insertions(+), 42 deletions(-) diff --git a/src/options/models/monte_carlo.rs b/src/options/models/monte_carlo.rs index d76c74f..cdd0ac4 100644 --- a/src/options/models/monte_carlo.rs +++ b/src/options/models/monte_carlo.rs @@ -222,7 +222,7 @@ impl OptionPricing for MonteCarloModel { OptionStyle::European => self.simulate_price_paths(option), OptionStyle::Basket => self.simulate_price_paths(option), OptionStyle::Rainbow(_) => self.simulate_price_paths(option), - OptionStyle::Barrier(_) => self.simulate_price_paths(option), + OptionStyle::Barrier(_) => self.price_barrier(option), OptionStyle::DoubleBarrier(_, _) => self.simulate_price_paths(option), OptionStyle::Asian(_) => self.price_asian(option), OptionStyle::Lookback(_) => self.price_asian(option), @@ -277,15 +277,61 @@ impl MonteCarloModel { }; // Now call payoff after the mutable borrow is done - sum += (-(self.risk_free_rate - option_clone.instrument().continuous_dividend_yield) - * option_clone.time_to_maturity()) - .exp() + sum += (-self.risk_free_rate * option_clone.time_to_maturity()).exp() * option_clone.payoff(Some(avg_price)); } // Return average payoff sum / self.simulations as f64 } + + /// Simulate price paths and compute the expected discounted payoff for Barrier options. + /// + /// # Arguments + /// + /// * `option` - The Barrier option to price. + /// + /// # Returns + /// + /// The expected discounted payoff of the option. + fn price_barrier(&self, option: &T) -> f64 { + let mut rng: ThreadRng = rand::rng(); + let mut sum = 0.0; + let mut option_clone = option.clone(); + + for _ in 0..self.simulations { + // Get a mutable reference to the instrument + let avg_price = { + let instrument = option_clone.instrument_mut(); + match self.method { + AvgMethod::Geometric => instrument.simulate_geometric_average_mut( + &mut rng, + SimMethod::Log, + self.volatility, + option.time_to_maturity(), + self.risk_free_rate, + self.steps, + ), + AvgMethod::Arithmetic => instrument.simulate_arithmetic_average_mut( + &mut rng, + SimMethod::Log, + self.volatility, + option.time_to_maturity(), + self.risk_free_rate, + self.steps, + ), + _ => panic!("Invalid averaging method"), + } + }; + + // Now call payoff after the mutable borrow is done + sum += (-self.risk_free_rate * option_clone.time_to_maturity()).exp() + * option_clone.payoff(None); + } + + // Return average payoff + sum / self.simulations as f64 + } } impl OptionStrategy for MonteCarloModel {} diff --git a/src/options/types/barrier_option.rs b/src/options/types/barrier_option.rs index ad6d13b..a919356 100644 --- a/src/options/types/barrier_option.rs +++ b/src/options/types/barrier_option.rs @@ -125,6 +125,27 @@ impl BarrierOption { _ => true, // Out-options don't require activation } } + + pub fn is_barrier_breached(&self, spot: f64) -> bool { + match self.barrier_type { + BarrierType::DownAndIn | BarrierType::DownAndOut => spot <= self.barrier, + BarrierType::UpAndIn | BarrierType::UpAndOut => spot >= self.barrier, + } + } + + pub fn is_in(&self) -> bool { + match self.barrier_type { + BarrierType::DownAndIn | BarrierType::UpAndIn => true, + BarrierType::DownAndOut | BarrierType::UpAndOut => false, + } + } + + pub fn is_out(&self) -> bool { + match self.barrier_type { + BarrierType::DownAndIn | BarrierType::UpAndIn => false, + BarrierType::DownAndOut | BarrierType::UpAndOut => true, + } + } } impl Option for BarrierOption { @@ -162,14 +183,14 @@ impl Option for BarrierOption { #[rustfmt::skip] fn payoff(&self, terminal: std::option::Option) -> f64 { - let terminal = terminal.unwrap_or_else(|| self.instrument.terminal_spot()); + let terminal = terminal.unwrap_or_else(|| self.instrument.terminal_spot()); let above = self.instrument.spot.iter().any(|&a| a >= self.barrier); let below = self.instrument.spot.iter().any(|&a| a <= self.barrier); let payoff = match self.option_type { OptionType::Call => (terminal - self.strike).max(0.0), OptionType::Put => (self.strike - terminal).max(0.0), }; - + match self.barrier_type { BarrierType::DownAndIn => if below { payoff } else { 0.0 }, BarrierType::DownAndOut => if below { 0.0 } else { payoff }, diff --git a/tests/options_pricing.rs b/tests/options_pricing.rs index 38c931d..bc4c070 100644 --- a/tests/options_pricing.rs +++ b/tests/options_pricing.rs @@ -1070,8 +1070,10 @@ mod binomial_tree_tests { use super::*; #[test] - fn test_down_and_in() { - let instrument = Instrument::new().with_spot(42.0).with_continuous_dividend_yield(0.08); + fn test_down_and_in_atm() { + let instrument = Instrument::new() + .with_spot(42.0) + .with_continuous_dividend_yield(0.08); let option = BarrierOption::down_and_in(instrument, 42.0, 42.0, 1.5, OptionType::Call); let model = BinomialTreeModel::new(0.05, 0.2, 252); @@ -1083,10 +1085,71 @@ mod binomial_tree_tests { } #[test] - fn test_down_and_out() { - let instrument = Instrument::new().with_spot(42.0); - let option = - BarrierOption::down_and_out(instrument, 100.0, 100.0, 2.0, OptionType::Call); + fn test_down_and_out_atm() { + let instrument = Instrument::new() + .with_spot(42.0) + .with_continuous_dividend_yield(0.08); + let option = BarrierOption::down_and_out(instrument, 42.0, 42.0, 1.5, OptionType::Call); + let model = BinomialTreeModel::new(0.05, 0.2, 252); + + let price = model.price(&option); + assert_abs_diff_eq!(price, 0.0000, epsilon = 0.0001); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 0.0000, epsilon = 0.0001); + } + + #[test] + fn test_down_and_in_otm() { + let instrument = Instrument::new() + .with_spot(35.0) + .with_continuous_dividend_yield(0.08); + let option = BarrierOption::down_and_in(instrument, 42.0, 20.0, 1.5, OptionType::Call); + let model = BinomialTreeModel::new(0.05, 0.2, 252); + + let price: f64 = model.price(&option); + assert_abs_diff_eq!(price, 0.0000, epsilon = 0.0001); // Shows price for Up-And-Out (0.0000) + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 0.89, epsilon = 0.0001); // Shows price for Up-And-Out (0.0000) + } + + #[test] + fn test_down_and_out_otm() { + let instrument = Instrument::new() + .with_spot(35.0) + .with_continuous_dividend_yield(0.08); + let option = BarrierOption::down_and_out(instrument, 42.0, 20.0, 1.5, OptionType::Call); + let model = BinomialTreeModel::new(0.05, 0.2, 252); + + let price = model.price(&option); + assert_abs_diff_eq!(price, 0.8078, epsilon = 0.0001); // Shows price for Up-And-In (0.8078) + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 7.84, epsilon = 0.0001); // Shows price for Up-And-In (8.7307) + } + + #[test] + fn test_down_and_in_itm() { + let instrument = Instrument::new() + .with_spot(54.0) + .with_continuous_dividend_yield(0.08); + let option = BarrierOption::down_and_in(instrument, 42.0, 60.0, 1.5, OptionType::Call); + let model = BinomialTreeModel::new(0.05, 0.2, 252); + + let price: f64 = model.price(&option); + assert_abs_diff_eq!(price, 10.1015, epsilon = 0.0001); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 1.1730, epsilon = 0.0001); + } + + #[test] + fn test_down_and_out_itm() { + let instrument = Instrument::new() + .with_spot(54.0) + .with_continuous_dividend_yield(0.08); + let option = BarrierOption::down_and_out(instrument, 42.0, 60.0, 1.5, OptionType::Call); let model = BinomialTreeModel::new(0.05, 0.2, 252); let price = model.price(&option); @@ -1097,9 +1160,11 @@ mod binomial_tree_tests { } #[test] - fn test_up_and_in() { - let instrument = Instrument::new().with_spot(42.0).with_continuous_dividend_yield(0.08); - let option = BarrierOption::up_and_in(instrument, 100.0, 100.0, 2.0, OptionType::Call); + fn test_up_and_in_atm() { + let instrument = Instrument::new() + .with_spot(42.0) + .with_continuous_dividend_yield(0.08); + let option = BarrierOption::up_and_in(instrument, 42.0, 42.0, 1.5, OptionType::Call); let model = BinomialTreeModel::new(0.05, 0.2, 252); let price: f64 = model.price(&option); @@ -1110,9 +1175,11 @@ mod binomial_tree_tests { } #[test] - fn test_up_and_out() { - let instrument = Instrument::new().with_spot(42.0); - let option = BarrierOption::up_and_out(instrument, 100.0, 100.0, 2.0, OptionType::Call); + fn test_up_and_out_atm() { + let instrument = Instrument::new() + .with_spot(42.0) + .with_continuous_dividend_yield(0.08); + let option = BarrierOption::up_and_out(instrument, 42.0, 42.0, 1.5, OptionType::Call); let model = BinomialTreeModel::new(0.05, 0.2, 252); let price = model.price(&option); @@ -1121,6 +1188,66 @@ mod binomial_tree_tests { let price = model.price(&option.flip()); assert_abs_diff_eq!(price, 0.0000, epsilon = 0.0001); } + + #[test] + fn test_up_and_in_otm() { + let instrument = Instrument::new() + .with_spot(35.0) + .with_continuous_dividend_yield(0.08); + let option = BarrierOption::up_and_in(instrument, 42.0, 20.0, 1.5, OptionType::Call); + let model = BinomialTreeModel::new(0.05, 0.2, 252); + + let price: f64 = model.price(&option); + assert_abs_diff_eq!(price, 0.8077, epsilon = 0.0001); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 8.7308, epsilon = 0.0001); + } + + #[test] + fn test_up_and_out_otm() { + let instrument = Instrument::new() + .with_spot(35.0) + .with_continuous_dividend_yield(0.08); + let option = BarrierOption::up_and_out(instrument, 42.0, 20.0, 1.5, OptionType::Call); + let model = BinomialTreeModel::new(0.05, 0.2, 252); + + let price = model.price(&option); + assert_abs_diff_eq!(price, 0.00, epsilon = 0.0001); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 0.00, epsilon = 0.0001); + } + + #[test] + fn test_up_and_in_itm() { + let instrument = Instrument::new() + .with_spot(54.0) + .with_continuous_dividend_yield(0.08); + let option = BarrierOption::up_and_in(instrument, 42.0, 60.0, 1.5, OptionType::Call); + let model = BinomialTreeModel::new(0.05, 0.2, 252); + + let price: f64 = model.price(&option); + assert_abs_diff_eq!(price, 8.78, epsilon = 0.0001); // Shows price for Down-And-Out (0.0) + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 0.16, epsilon = 0.0001); // Shows price for Down-And-Out + } + + #[test] + fn test_up_and_out_itm() { + let instrument = Instrument::new() + .with_spot(54.0) + .with_continuous_dividend_yield(0.08); + let option = BarrierOption::up_and_out(instrument, 42.0, 60.0, 1.5, OptionType::Call); + let model = BinomialTreeModel::new(0.05, 0.2, 252); + + let price = model.price(&option); + assert_abs_diff_eq!(price, 1.32, epsilon = 0.0001); // Shows price for Down-And-In (10.1015) + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 1.01, epsilon = 0.0001); // Shows price for Down-And-In + } } #[test] @@ -1595,56 +1722,183 @@ mod monte_carlo_tests { use super::*; #[test] - fn test_down_and_in() { - let instrument = Instrument::new().with_spot(42.0); + fn test_down_and_in_atm() { + let instrument = Instrument::new() + .with_spot(42.0) + .with_continuous_dividend_yield(0.08); let option = BarrierOption::down_and_in(instrument, 42.0, 42.0, 1.5, OptionType::Call); - let model = MonteCarloModel::geometric(0.05, 0.2, 4_000, 20); + let model = MonteCarloModel::geometric(0.05, 0.2, 4_000, 252); let price: f64 = model.price(&option); - assert_abs_diff_eq!(price, 5.65, epsilon = 0.0001); + assert_abs_diff_eq!(price, 2.9158, epsilon = 0.5); let price = model.price(&option.flip()); - assert_abs_diff_eq!(price, 2.61, epsilon = 0.0001); + assert_abs_diff_eq!(price, 4.6304, epsilon = 0.5); } #[test] - fn test_down_and_out() { - let instrument = Instrument::new().with_spot(42.0); - let option = - BarrierOption::down_and_out(instrument, 100.0, 100.0, 2.0, OptionType::Call); - let model = MonteCarloModel::geometric(0.05, 0.2, 4_000, 20); + fn test_down_and_out_atm() { + let instrument = Instrument::new() + .with_spot(42.0) + .with_continuous_dividend_yield(0.08); + let option = BarrierOption::down_and_out(instrument, 42.0, 42.0, 1.5, OptionType::Call); + let model = MonteCarloModel::geometric(0.05, 0.2, 4_000, 252); + + let price = model.price(&option); + assert_abs_diff_eq!(price, 0.0000, epsilon = 0.5); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 0.0000, epsilon = 0.5); + } + + #[test] + fn test_down_and_in_otm() { + let instrument = Instrument::new() + .with_spot(35.0) + .with_continuous_dividend_yield(0.08); + let option = BarrierOption::down_and_in(instrument, 42.0, 20.0, 1.5, OptionType::Call); + let model = MonteCarloModel::geometric(0.05, 0.2, 4_000, 252); + + let price: f64 = model.price(&option); + assert_abs_diff_eq!(price, 0.0000, epsilon = 0.5); // Shows price for Up-And-Out (0.0000) + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 0.89, epsilon = 0.5); // Shows price for Up-And-Out (0.0000) + } + + #[test] + fn test_down_and_out_otm() { + let instrument = Instrument::new() + .with_spot(35.0) + .with_continuous_dividend_yield(0.08); + let option = BarrierOption::down_and_out(instrument, 42.0, 20.0, 1.5, OptionType::Call); + let model = MonteCarloModel::geometric(0.05, 0.2, 4_000, 252); + + let price = model.price(&option); + assert_abs_diff_eq!(price, 0.8078, epsilon = 0.5); // Shows price for Up-And-In (0.8078) + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 7.84, epsilon = 0.5); // Shows price for Up-And-In (8.7307) + } + + #[test] + fn test_down_and_in_itm() { + let instrument = Instrument::new() + .with_spot(54.0) + .with_continuous_dividend_yield(0.08); + let option = BarrierOption::down_and_in(instrument, 42.0, 60.0, 1.5, OptionType::Call); + let model = MonteCarloModel::geometric(0.05, 0.2, 4_000, 252); + + let price: f64 = model.price(&option); + assert_abs_diff_eq!(price, 10.1015, epsilon = 0.5); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 1.1730, epsilon = 0.5); + } + + #[test] + fn test_down_and_out_itm() { + let instrument = Instrument::new() + .with_spot(54.0) + .with_continuous_dividend_yield(0.08); + let option = BarrierOption::down_and_out(instrument, 42.0, 60.0, 1.5, OptionType::Call); + let model = MonteCarloModel::geometric(0.05, 0.2, 4_000, 252); let price = model.price(&option); - assert_abs_diff_eq!(price, 0.0000, epsilon = 1.0); + assert_abs_diff_eq!(price, 0.0000, epsilon = 0.5); let price = model.price(&option.flip()); - assert_abs_diff_eq!(price, 0.0000, epsilon = 1.0); + assert_abs_diff_eq!(price, 0.0000, epsilon = 0.5); } #[test] - fn test_up_and_in() { - let instrument = Instrument::new().with_spot(42.0); - let option = BarrierOption::up_and_in(instrument, 100.0, 100.0, 2.0, OptionType::Call); - let model = MonteCarloModel::geometric(0.05, 0.2, 4_000, 20); + fn test_up_and_in_atm() { + let instrument = Instrument::new() + .with_spot(42.0) + .with_continuous_dividend_yield(0.08); + let option = BarrierOption::up_and_in(instrument, 42.0, 42.0, 1.5, OptionType::Call); + let model = MonteCarloModel::geometric(0.05, 0.2, 4_000, 252); let price: f64 = model.price(&option); - assert_abs_diff_eq!(price, 5.65, epsilon = 0.0001); + assert_abs_diff_eq!(price, 2.9158, epsilon = 0.5); let price = model.price(&option.flip()); - assert_abs_diff_eq!(price, 2.61, epsilon = 0.0001); + assert_abs_diff_eq!(price, 4.6304, epsilon = 0.5); } #[test] - fn test_up_and_out() { - let instrument = Instrument::new().with_spot(42.0); - let option = BarrierOption::up_and_out(instrument, 100.0, 100.0, 2.0, OptionType::Call); - let model = MonteCarloModel::geometric(0.05, 0.2, 4_000, 20); + fn test_up_and_out_atm() { + let instrument = Instrument::new() + .with_spot(42.0) + .with_continuous_dividend_yield(0.08); + let option = BarrierOption::up_and_out(instrument, 42.0, 42.0, 1.5, OptionType::Call); + let model = MonteCarloModel::geometric(0.05, 0.2, 4_000, 252); + + let price = model.price(&option); + assert_abs_diff_eq!(price, 0.0000, epsilon = 0.5); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 0.0000, epsilon = 0.5); + } + + #[test] + fn test_up_and_in_otm() { + let instrument = Instrument::new() + .with_spot(35.0) + .with_continuous_dividend_yield(0.08); + let option = BarrierOption::up_and_in(instrument, 42.0, 20.0, 1.5, OptionType::Call); + let model = MonteCarloModel::geometric(0.05, 0.2, 4_000, 252); + + let price: f64 = model.price(&option); + assert_abs_diff_eq!(price, 0.8077, epsilon = 0.5); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 8.7308, epsilon = 0.5); + } + + #[test] + fn test_up_and_out_otm() { + let instrument = Instrument::new() + .with_spot(35.0) + .with_continuous_dividend_yield(0.08); + let option = BarrierOption::up_and_out(instrument, 42.0, 20.0, 1.5, OptionType::Call); + let model = MonteCarloModel::geometric(0.05, 0.2, 4_000, 252); + + let price = model.price(&option); + assert_abs_diff_eq!(price, 0.00, epsilon = 0.5); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 0.00, epsilon = 0.5); + } + + #[test] + fn test_up_and_in_itm() { + let instrument = Instrument::new() + .with_spot(54.0) + .with_continuous_dividend_yield(0.08); + let option = BarrierOption::up_and_in(instrument, 42.0, 60.0, 1.5, OptionType::Call); + let model = MonteCarloModel::geometric(0.05, 0.2, 4_000, 252); + + let price: f64 = model.price(&option); + assert_abs_diff_eq!(price, 8.78, epsilon = 0.5); // Shows price for Down-And-Out (0.0) + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 0.16, epsilon = 0.5); // Shows price for Down-And-Out + } + + #[test] + fn test_up_and_out_itm() { + let instrument = Instrument::new() + .with_spot(54.0) + .with_continuous_dividend_yield(0.08); + let option = BarrierOption::up_and_out(instrument, 42.0, 60.0, 1.5, OptionType::Call); + let model = MonteCarloModel::geometric(0.05, 0.2, 4_000, 252); let price = model.price(&option); - assert_abs_diff_eq!(price, 0.0000, epsilon = 1.0); + assert_abs_diff_eq!(price, 1.32, epsilon = 0.5); // Shows price for Down-And-In (10.1015) let price = model.price(&option.flip()); - assert_abs_diff_eq!(price, 0.0000, epsilon = 1.0); + assert_abs_diff_eq!(price, 1.01, epsilon = 0.5); // Shows price for Down-And-In } } } From b4ed59248262daf472797602faebf432bec6b3c3 Mon Sep 17 00:00:00 2001 From: carlobortolan Date: Mon, 31 Mar 2025 02:19:22 +0200 Subject: [PATCH 08/10] Fix doc typos --- src/options/types/barrier_option.rs | 4 +++- src/options/types/bermudan_option.rs | 2 +- src/options/types/lookback_option.rs | 3 +++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/options/types/barrier_option.rs b/src/options/types/barrier_option.rs index a919356..9c47894 100644 --- a/src/options/types/barrier_option.rs +++ b/src/options/types/barrier_option.rs @@ -1,11 +1,13 @@ //! Module for Barrier option type. +//! +//! https://coggit.com/freetools use std::any::Any; use super::{BarrierType, OptionStyle, OptionType}; use crate::options::{Instrument, Option}; -/// A struct representing an Bermudan option. +/// A struct representing a Barrier option. #[derive(Clone, Debug)] pub struct BarrierOption { /// The underlying instrument. diff --git a/src/options/types/bermudan_option.rs b/src/options/types/bermudan_option.rs index 596489a..4b7ed0a 100644 --- a/src/options/types/bermudan_option.rs +++ b/src/options/types/bermudan_option.rs @@ -12,7 +12,7 @@ use crate::{ options::{Instrument, Option}, }; -/// A struct representing an Bermudan option. +/// A struct representing a Bermudan option. #[derive(Clone, Debug)] pub struct BermudanOption { /// The underlying instrument. diff --git a/src/options/types/lookback_option.rs b/src/options/types/lookback_option.rs index d571128..d61650b 100644 --- a/src/options/types/lookback_option.rs +++ b/src/options/types/lookback_option.rs @@ -1,5 +1,8 @@ +//! Module for Lookback option type. + use crate::options::{types::Permutation, Instrument, Option, OptionStyle, OptionType}; +/// A struct representing a Lookback option. #[derive(Clone, Debug)] pub struct LookbackOption { /// The underlying instrument. From cb915aa0cd88566d4a705e9f019dab1950aae7aa Mon Sep 17 00:00:00 2001 From: carlobortolan Date: Mon, 31 Mar 2025 02:22:04 +0200 Subject: [PATCH 09/10] Update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 35eef0b..947eb9b 100644 --- a/README.md +++ b/README.md @@ -41,12 +41,12 @@ Quantrs supports options pricing with various models for both vanilla and exotic | Bermudan | ❌ | ❌ | ✅ | ❌ (L. Sq.) | ❌ (complex) | ❌ | | ¹Basket | ⏳ (∀component) | ❌ | ⏳ (approx.) | ⏳ | ❌ | ❌ | | ¹Rainbow | ✅ (∀component) | ❌ | ✅ | ✅ | ❌ | ❌ | -| ²Barrier | ❌ (mod. BSM) | ❌ | ✅ | ✅ | ⏳ | ⏳ | +| ²Barrier | ❌ (mod. BSM) | ❌ | ✅ (flaky) | ✅ | ⏳ | ⏳ | | ²Double Barrier | ❌ (mod. BSM) | ❌ | ⏳ | ⏳ | ❌ (complex) | ⏳ | | ²Asian (fixed strike) | ❌ (mod. BSM) | ❌ | ❌ | ✅ | ⏳ | ⏳ | | ²Asian (floating strike) | ❌ (mod. BSM) | ❌ | ❌ | ✅ | ⏳ | ⏳ | | ²Lookback (fixed strike) | ❌ | ❌ | ❌ | ✅ | ⏳ | ⏳ | -| ²Lookback (floating strike) | ✅ (!spots) | ❌ | ❌ | ✅ | ⏳ | ⏳ | +| ²Lookback (floating strike) | ✅ | ❌ | ❌ | ✅ | ⏳ | ⏳ | | ²Binary Cash-or-Nothing | ✅ | ❌ | ✅ | ✅ | ❌ (mod. PDE) | ⏳ | | ²Binary Asset-or-Nothing | ✅ | ❌ | ✅ | ✅ | ❌ (mod. PDE) | ⏳ | | Greeks (Δ,ν,Θ,ρ,Γ) | ✅ | ✅ | ⏳ | ❌ | ❌ | ❌ | From a905abab7489613cfb734e49d164afb7fc75b188 Mon Sep 17 00:00:00 2001 From: carlobortolan Date: Mon, 31 Mar 2025 02:29:59 +0200 Subject: [PATCH 10/10] Cargo.lock --- Cargo.lock | 25 +++++----- src/options/models/binomial_tree.rs | 74 ++++++++++++++++++++++++++++- 2 files changed, 85 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 71faa25..364862c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,18 +153,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.32" +version = "4.5.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" +checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.32" +version = "4.5.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" +checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489" dependencies = [ "anstyle", "clap_lex", @@ -342,9 +342,9 @@ dependencies = [ [[package]] name = "dwrote" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70182709525a3632b2ba96b6569225467b18ecb4a77f46d255f713a6bebf05fd" +checksum = "bfe1f192fcce01590bd8d839aca53ce0d11d803bf291b2a6c4ad925a8f0024be" dependencies = [ "lazy_static", "libc", @@ -497,14 +497,15 @@ checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", "windows-core", ] @@ -614,9 +615,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.26" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "matrixmultiply" @@ -712,9 +713,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.1" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "oorandom" diff --git a/src/options/models/binomial_tree.rs b/src/options/models/binomial_tree.rs index a5ed87f..33865ce 100644 --- a/src/options/models/binomial_tree.rs +++ b/src/options/models/binomial_tree.rs @@ -104,10 +104,82 @@ impl BinomialTreeModel { steps, } } + + fn price_barrier(&self, option: &T) -> f64 { + let dt = option.time_to_maturity() / self.steps as f64; + let u = (self.volatility * dt.sqrt()).exp(); + let d = 1.0 / u; + let p = (((self.risk_free_rate - option.instrument().continuous_dividend_yield) * dt) + .exp() + - d) + / (u - d); + let discount_factor = (-self.risk_free_rate * dt).exp(); + + let barrier_option = option.as_any().downcast_ref::(); + let is_barrier = barrier_option.is_some(); + + // Initialize option values at maturity + let mut option_values: Vec<(f64, bool)> = (0..=self.steps) + .map(|i| { + let mut path = vec![]; + let spot = + option.instrument().spot() * u.powi(i as i32) * d.powi((self.steps - i) as i32); + path.push(spot); + + let mut value = option.payoff(Some(spot)); + let mut activated = false; + + if let Some(barrier_opt) = &barrier_option { + if barrier_opt.is_knocked_out(spot) { + value = 0.0; // Knocked-out options are worthless + } else if barrier_opt.is_activated(&path) { + activated = true; // Track knock-in activation + } else if barrier_opt.is_in() && !activated { + value = 0.0; // Knock-in options are worthless unless activated + } + } + + (value, activated) + }) + .collect(); + + // Backward induction + for step in (0..self.steps).rev() { + for i in 0..=step { + let mut path = vec![]; + let spot = + option.instrument().spot() * u.powi(i as i32) * d.powi((step - i) as i32); + path.push(spot); + + let expected_value = + discount_factor * (p * option_values[i + 1].0 + (1.0 - p) * option_values[i].0); + + if let Some(barrier_opt) = &barrier_option { + if barrier_opt.is_knocked_out(spot) { + option_values[i] = (0.0, false); // Knocked-out options are worthless + } else if barrier_opt.is_activated(&path) { + option_values[i] = (expected_value, true); + } else if barrier_opt.is_in() && !option_values[i].1 { + option_values[i] = (0.0, false); // Knock-in options are worthless unless activated + } else { + option_values[i] = (expected_value, option_values[i].1); + } + } else { + option_values[i] = (expected_value, false); + } + } + } + + option_values[0].0 + } } impl OptionPricing for BinomialTreeModel { fn price(&self, option: &T) -> f64 { + if matches!(option.style(), OptionStyle::Barrier(_)) { + return self.price_barrier(option); + } + // Multiplicative up-/downward movements of an asset in a single step of the binomial tree let dt = option.time_to_maturity() / self.steps as f64; let u = (self.volatility * dt.sqrt()).exp(); @@ -135,10 +207,8 @@ impl OptionPricing for BinomialTreeModel { // Backward induction for step in (0..self.steps).rev() { for i in 0..=step { - let mut path = vec![]; let spot = option.instrument().spot() * u.powi(i as i32) * d.powi((step - i) as i32); - path.push(spot); let expected_value = discount_factor * (p * option_values[i + 1] + (1.0 - p) * option_values[i]);