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/README.md b/README.md index 2b234d2..947eb9b 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ 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) | ❌ | ❌ | ✅ | ⏳ | ⏳ | diff --git a/src/options/models/binomial_tree.rs b/src/options/models/binomial_tree.rs index 0b25dde..33865ce 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)] @@ -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,9 +207,13 @@ impl OptionPricing for BinomialTreeModel { // Backward induction for step in (0..self.steps).rev() { for i in 0..=step { + let spot = + option.instrument().spot() * u.powi(i as i32) * d.powi((step - i) as i32); + 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/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/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..9c47894 --- /dev/null +++ b/src/options/types/barrier_option.rs @@ -0,0 +1,222 @@ +//! 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 a Barrier 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, + } + } + + 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, + ) + } + + 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 + } + } + + 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 { + 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) + } + + #[rustfmt::skip] + 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 => 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 }, + } + } + + 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..d61650b 100644 --- a/src/options/types/lookback_option.rs +++ b/src/options/types/lookback_option.rs @@ -1,13 +1,20 @@ +//! 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. 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 +24,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 +96,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 +130,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), diff --git a/tests/options_pricing.rs b/tests/options_pricing.rs index c4be08f..bc4c070 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 {} @@ -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,190 @@ mod binomial_tree_tests { } } + mod barrier_option_tests { + use super::*; + + #[test] + 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); + + 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_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); + 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_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); + 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_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); + 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_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] fn test_binomial_tree_iv() { let instrument = Instrument::new().with_spot(100.0); @@ -1532,6 +1717,190 @@ 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_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, 252); + + let price: f64 = model.price(&option); + assert_abs_diff_eq!(price, 2.9158, epsilon = 0.5); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 4.6304, epsilon = 0.5); + } + + #[test] + 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 = 0.5); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 0.0000, epsilon = 0.5); + } + + #[test] + 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, 2.9158, epsilon = 0.5); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 4.6304, epsilon = 0.5); + } + + #[test] + 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, 1.32, epsilon = 0.5); // Shows price for Down-And-In (10.1015) + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 1.01, epsilon = 0.5); // Shows price for Down-And-In + } + } } // Black-76 Model Tests @@ -1983,6 +2352,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,