diff --git a/README.md b/README.md index d7b1cb5..1791379 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,8 @@ Quantrs supports options pricing with various models for both vanilla and exotic | ²Double Barrier | ❌ (mod. BSM) | ❌ | ⏳ | ⏳ | ❌ (complex) | ⏳ | | ²Asian (fixed strike) | ❌ (mod. BSM) | ❌ | ❌ | ✅ | ⏳ | ⏳ | | ²Asian (floating strike) | ❌ (mod. BSM) | ❌ | ❌ | ✅ | ⏳ | ⏳ | -| ²Lookback (fixed strike) | ⏳ | ❌ | ❌ | ⏳ | ⏳ | ⏳ | -| ²Lookback (floating strike) | ⏳ | ❌ | ❌ | ⏳ | ⏳ | ⏳ | +| ²Lookback (fixed strike) | ❌ | ❌ | ❌ | ✅ | ⏳ | ⏳ | +| ²Lookback (floating strike) | ✅ | ❌ | ❌ | ✅ | ⏳ | ⏳ | | ²Binary Cash-or-Nothing | ✅ | ❌ | ✅ | ✅ | ❌ (mod. PDE) | ⏳ | | ²Binary Asset-or-Nothing | ✅ | ❌ | ✅ | ✅ | ❌ (mod. PDE) | ⏳ | | Greeks (Δ,ν,Θ,ρ,Γ) | ✅ | ✅ | ⏳ | ❌ | ❌ | ❌ | @@ -95,7 +95,7 @@ Add this to your `Cargo.toml`: quantrs = "0.1.6" ``` -Now if you want to e.g., model binary call options using the Black-Scholes model, you can: +Now if you want to e.g., calculate the arbitrage-free price of a binary cash-or-nothing call using the Black-Scholes model, you can: ```rust use quantrs::options::*; @@ -131,7 +131,7 @@ Greeks { delta: 0.013645840354947947, gamma: -0.0008813766475726433, theta: 0.17 Quantrs also supports plotting option prices and strategies using the `plotters` backend. -E.g., Plot the P/L of a slightly skewed Condor spread using the Monte-Carlo model: +E.g., Plot the P/L of a slightly skewed Condor spread consisting of fixed-strike Asian calls using the Monte-Carlo model with the geometric average price path:
Click to see example code @@ -142,20 +142,20 @@ use quantrs::options::*; // Create a new instrument with a spot price of 100 and a dividend yield of 2% let instrument = Instrument::new().with_spot(100.0).with_cont_yield(0.02); -// Create a vector of European call options with different strike prices +// Create a vector of fixed-strike Asian calls options with different strike prices let options = vec![ - EuropeanOption::new(instrument.clone(), 85.0, 1.0, Call), - EuropeanOption::new(instrument.clone(), 95.0, 1.0, Call), - EuropeanOption::new(instrument.clone(), 102.0, 1.0, Call), - EuropeanOption::new(instrument.clone(), 115.0, 1.0, Call), + AsianOption::fixed(instrument.clone(), 85.0, 1.0, Call), + AsianOption::fixed(instrument.clone(), 95.0, 1.0, Call), + AsianOption::fixed(instrument.clone(), 102.0, 1.0, Call), + AsianOption::fixed(instrument.clone(), 115.0, 1.0, Call), ]; // Create a new Monte-Carlo model with: // - Risk-free interest rate (r) = 5% // - Volatility (σ) = 20% // - Number of simulations = 10,000 -// - Number of time steps = 365 -let model = MonteCarloModel::geometric(0.05, 0.2, 10_000, 365); +// - Number of time steps = 252 +let model = MonteCarloModel::geometric(0.05, 0.2, 10_000, 252); // Plot a breakdown of the Condor spread with a spot price range of [80,120] model.plot_strategy_breakdown( diff --git a/examples/images/strategy.png b/examples/images/strategy.png index c5fb3ff..670f3e2 100644 Binary files a/examples/images/strategy.png and b/examples/images/strategy.png differ diff --git a/examples/options_pricing.rs b/examples/options_pricing.rs index 513a77d..3a546f3 100644 --- a/examples/options_pricing.rs +++ b/examples/options_pricing.rs @@ -711,24 +711,24 @@ fn example_strategy() { } fn example_plots() { - // Create a new Monte-Carlo model with: - // - Risk-free interest rate (r) = 5% - // - Volatility (σ) = 20% - // - Number of simulations = 10,000 - // - Number of time steps = 365 - let model = MonteCarloModel::geometric(0.05, 0.2, 10_000, 365); - // Create a new instrument with a spot price of 100 and a dividend yield of 2% let instrument = Instrument::new().with_spot(100.0).with_cont_yield(0.02); - // Create a vector of European call options with different strike prices + // Create a vector of fixed-strike Asian calls options with different strike prices let options = vec![ - EuropeanOption::new(instrument.clone(), 85.0, 1.0, Call), - EuropeanOption::new(instrument.clone(), 95.0, 1.0, Call), - EuropeanOption::new(instrument.clone(), 102.0, 1.0, Call), - EuropeanOption::new(instrument.clone(), 115.0, 1.0, Call), + AsianOption::fixed(instrument.clone(), 85.0, 1.0, Call), + AsianOption::fixed(instrument.clone(), 95.0, 1.0, Call), + AsianOption::fixed(instrument.clone(), 102.0, 1.0, Call), + AsianOption::fixed(instrument.clone(), 115.0, 1.0, Call), ]; + // Create a new Monte-Carlo model with: + // - Risk-free interest rate (r) = 5% + // - Volatility (σ) = 20% + // - Number of simulations = 10,000 + // - Number of time steps = 252 + let model = MonteCarloModel::geometric(0.05, 0.2, 10_000, 252); + // Plot a breakdown of the Condor spread with a spot price range of [80,120] let _ = model.plot_strategy_breakdown( "Condor Example", diff --git a/src/options/instrument.rs b/src/options/instrument.rs index f3940f9..b707f46 100644 --- a/src/options/instrument.rs +++ b/src/options/instrument.rs @@ -30,10 +30,6 @@ use rand_distr::{Distribution, Normal}; pub struct Instrument { /// Current price of the underlying asset or future price at time 0. pub spot: Vec, - /// Maximum spot price of the underlying asset. - pub max_spot: f64, - /// Minimum spot price of the underlying asset. - pub min_spot: f64, /// Continuous dividend yield where the dividend amount is proportional to the level of the underlying asset (e.g., 0.02 for 2%). pub continuous_dividend_yield: f64, /// Discrete proportional dividend yield (e.g., 0.02 for 2%). @@ -55,8 +51,6 @@ impl Instrument { pub fn new() -> Self { Self { spot: vec![0.0], - max_spot: 0.0, - min_spot: 0.0, continuous_dividend_yield: 0.0, discrete_dividend_yield: 0.0, dividend_times: Vec::new(), @@ -93,32 +87,22 @@ impl Instrument { self } - /// Set the maximum spot price of the instrument. + /// Get the maximum spot price of the instrument. /// - /// # Arguments - /// - /// * `max_spot` - The maximum spot price of the instrument. - /// /// # Returns /// - /// The instrument with the maximum spot price set. - pub fn with_max_spot(mut self, max_spot: f64) -> Self { - self.max_spot = max_spot; - self + /// The maximum spot price of the instrument. + pub fn max_spot(&self) -> f64 { + *self.spot.iter().max_by(|x, y| x.total_cmp(y)).unwrap() } - /// Set the minimum spot price of the instrument. - /// - /// # Arguments - /// - /// * `min_spot` - The minimum spot price of the instrument. + /// Get the minimum spot price of the instrument. /// /// # Returns /// - /// The instrument with the minimum spot price set. - pub fn with_min_spot(mut self, min_spot: f64) -> Self { - self.min_spot = min_spot; - self + /// The minimum spot price of the instrument. + pub fn min_spot(&self) -> f64 { + *self.spot.iter().min_by(|x, y| x.total_cmp(y)).unwrap() } /// Set the continuous dividend yield of the instrument. diff --git a/src/options/models/black_scholes.rs b/src/options/models/black_scholes.rs index 2544607..b8a73c5 100644 --- a/src/options/models/black_scholes.rs +++ b/src/options/models/black_scholes.rs @@ -15,48 +15,14 @@ //! including the current price of the underlying asset, the strike price of the option, the time to expiration, the risk-free interest rate, //! and the volatility of the underlying asset. //! -//! ## Formula -//! -//! The price of an option using the Black-Scholes model is calculated as follows: -//! -//! ```text -//! C = S * N(d1) - X * e^(-rT) * N(d2) for a call option -//! P = X * e^(-rT) * N(-d2) - S * N(-d1) for a put option -//! ``` -//! -//! where: -//! - `C` is the price of the call option. -//! - `P` is the price of the put option. -//! - `S` is the current price of the underlying asset. -//! - `X` is the strike price of the option. -//! - `r` is the risk-free interest rate. -//! - `T` is the time to maturity. -//! - `N` is the cumulative distribution function of the standard normal distribution. -//! - `d1` and `d2` are calculated as follows: -//! ```text -//! d1 = (ln(S / X) + (r + 0.5 * σ^2) * T) / (σ * sqrt(T)) -//! d2 = d1 - σ * sqrt(T) -//! ``` -//! - `σ` is the volatility of the underlying asset. -//! -//! The payoff of the option is calculated as: -//! -//! ```text -//! payoff = max(ST - K, 0) for a call option -//! payoff = max(K - ST, 0) for a put option -//! ``` -//! -//! where: -//! - `ST` is the price of the underlying asset at maturity. -//! - `K` is the strike price of the option. -//! - `max` is the maximum function. -//! //! ## References //! //! - [Wikipedia - Black-Scholes model](https://en.wikipedia.org/wiki/Black%E2%80%93Scholes_model) //! - [Black-Scholes Calculator](https://www.math.drexel.edu/~pg/fin/VanillaCalculator.html) //! - [Cash or Nothing Options' Greeks](https://quantpie.co.uk/bsm_bin_c_formula/bs_bin_c_summary.php) //! - [Asset or Nothing Options' Greeks](https://quantpie.co.uk/bsm_bin_a_formula/bs_bin_a_summary.php) +//! - Musiela, M., Rutkowski, M. Martingale Methods in Financial Modelling, 2nd Ed Springer, 2007 +//! - Joshi, M. The Concepts and Practice of Mathematical Finance, 2nd Ed Cambridge University Press, 2008 //! //! ## Example //! @@ -74,8 +40,9 @@ use crate::options::{ types::BinaryType::{AssetOrNothing, CashOrNothing}, Instrument, Option, OptionGreeks, OptionPricing, OptionStrategy, OptionStyle, OptionType, - RainbowType, + Permutation, RainbowType, }; +use rand_distr::num_traits::Pow; use statrs::distribution::{Continuous, ContinuousCDF, Normal}; /// A struct representing a Black-Scholes model. @@ -297,6 +264,57 @@ impl BlackScholesModel { ) } + /// Calculate the price of a lookback option using the Black-Scholes formula. + /// + /// # Arguments + /// + /// * `option` - The option to price. + /// * `normal` - The standard normal distribution. + /// + /// # Returns + /// + /// The price of the option. + pub fn price_lookback(&self, option: &T, normal: &Normal) -> f64 { + let max = option.instrument().max_spot(); + let min = option.instrument().min_spot(); + let sqrt_t = option.time_to_maturity().sqrt(); + let s = option.instrument().spot(); + let r = self.risk_free_rate; + let t = option.time_to_maturity(); + let vola = self.volatility; + + assert!(s > 0.0 && max > 0.0 && min > 0.0, "Spot prices must be > 0"); + + println!("max: {}, min: {}", max, min); + + let a1 = |s: f64, h: f64| ((s / h).ln() + (r + 0.5 * vola.powi(2)) * t) / (vola * sqrt_t); + let a2 = |s: f64, h: f64| a1(s, h) - vola * sqrt_t; + let a3 = |s: f64, h: f64| a1(s, h) - 2.0 * r * sqrt_t / vola; + + let phi = |x: f64| normal.cdf(x); + + match option.option_type() { + OptionType::Call => { + s * phi(a1(s, min)) + - min * (-r * t).exp() * phi(a2(s, min)) + - (0.5 * s * vola.powi(2)) / (r) + * (phi(-a1(s, min)) + - (-r * t).exp() + * (min / s).pow((2f64 * r) / (vola.powi(2))) + * phi(-a3(s, min))) + } + OptionType::Put => { + -s * phi(-a1(s, max)) + + max * (-r * t).exp() * phi(-a2(s, max)) + + (0.5 * s * vola.powi(2)) / (r) + * (phi(a1(s, max)) + - (-r * t).exp() + * (max / s).pow((2f64 * r) / (vola.powi(2))) + * phi(a3(s, max))) + } + } + } + /// Calculate the option price using the Black-Scholes formula with a given volatility. /// /// # Arguments @@ -369,6 +387,7 @@ impl OptionPricing for BlackScholesModel { (_, OptionStyle::Binary(AssetOrNothing)) => self.price_asset_or_nothing(option, &normal), (OptionType::Call, OptionStyle::Rainbow(_)) => self.price_rainbow_call(option, &normal), (OptionType::Put, OptionStyle::Rainbow(_)) => self.price_rainbow_put(option, &normal), + (_, OptionStyle::Lookback(Permutation::Floating)) => self.price_lookback(option, &normal), _ => panic!("BlackScholesModel does not support this option type or style"), } } diff --git a/src/options/models/monte_carlo.rs b/src/options/models/monte_carlo.rs index dac2fcc..d76c74f 100644 --- a/src/options/models/monte_carlo.rs +++ b/src/options/models/monte_carlo.rs @@ -182,13 +182,32 @@ impl MonteCarloModel { .into_par_iter() // Rayon parallel iterator .map(|_| { let mut rng = rand::rng(); - let simulated_price = option.instrument().simulate_geometric_brownian_motion( - &mut rng, - self.volatility, - option.time_to_maturity(), - self.risk_free_rate, - self.steps, - ); + let simulated_price = match self.method { + AvgMethod::Geometric => option.instrument().simulate_geometric_average( + &mut rng, + SimMethod::Log, + self.volatility, + option.time_to_maturity(), + self.risk_free_rate, + self.steps, + ), + AvgMethod::Arithmetic => option.instrument().simulate_arithmetic_average( + &mut rng, + SimMethod::Log, + self.volatility, + option.time_to_maturity(), + self.risk_free_rate, + self.steps, + ), + AvgMethod::Brownian => option.instrument().simulate_geometric_brownian_motion( + &mut rng, + self.volatility, + option.time_to_maturity(), + self.risk_free_rate, + self.steps, + ), + }; + option.payoff(Some(simulated_price)) }) .sum(); @@ -200,14 +219,14 @@ impl MonteCarloModel { impl OptionPricing for MonteCarloModel { fn price(&self, option: &T) -> f64 { match option.style() { - OptionStyle::European => self.price_european(option), - OptionStyle::Basket => self.price_basket(option), - OptionStyle::Rainbow(_) => self.price_rainbow(option), - OptionStyle::Barrier(_) => self.price_barrier(option), - OptionStyle::DoubleBarrier(_, _) => self.price_double_barrier(option), + 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::DoubleBarrier(_, _) => self.simulate_price_paths(option), OptionStyle::Asian(_) => self.price_asian(option), - OptionStyle::Lookback(_) => self.price_lookback(option), - OptionStyle::Binary(_) => self.price_binary(option), + OptionStyle::Lookback(_) => self.price_asian(option), + OptionStyle::Binary(_) => self.simulate_price_paths(option), _ => panic!("Monte Carlo model does not support this option style"), } } @@ -218,19 +237,6 @@ impl OptionPricing for MonteCarloModel { } impl MonteCarloModel { - /// Simulate price paths and compute the expected discounted payoff for European options. - /// - /// # Arguments - /// - /// * `option` - The European option to price. - /// - /// # Returns - /// - /// The expected discounted payoff of the option. - fn price_european(&self, option: &T) -> f64 { - self.simulate_price_paths(option) - } - /// Simulate price paths and compute the expected discounted payoff for Asian options. /// /// # Arguments @@ -280,84 +286,6 @@ impl MonteCarloModel { // Return average payoff sum / self.simulations as f64 } - - /// Simulate price paths and compute the expected discounted payoff for basket options. - /// - /// # Arguments - /// - /// * `option` - The basket option to price. - /// - /// # Returns - /// - /// The expected discounted payoff of the option. - fn price_basket(&self, option: &T) -> f64 { - unimplemented!() - } - - /// Simulate price paths and compute the expected discounted payoff for rainbow options. - /// - /// # Arguments - /// - /// * `option` - The rainbow option to price. - /// - /// # Returns - /// - /// The expected discounted payoff of the option. - fn price_rainbow(&self, option: &T) -> f64 { - self.simulate_price_paths(option) - } - - /// 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 { - unimplemented!() - } - - /// Simulate price paths and compute the expected discounted payoff for double barrier options. - /// - /// # Arguments - /// - /// * `option` - The double barrier option to price. - /// - /// # Returns - /// - /// The expected discounted payoff of the option. - fn price_double_barrier(&self, option: &T) -> f64 { - unimplemented!() - } - - /// Simulate price paths and compute the expected discounted payoff for lookback options. - /// - /// # Arguments - /// - /// * `option` - The lookback option to price. - /// - /// # Returns - /// - /// The expected discounted payoff of the option. - fn price_lookback(&self, option: &T) -> f64 { - unimplemented!() - } - - /// Simulate price paths and compute the expected discounted payoff for binary options. - /// - /// # Arguments - /// - /// * `option` - The binary option to price. - /// - /// # Returns - /// - /// The expected discounted payoff of the option. - fn price_binary(&self, option: &T) -> f64 { - self.simulate_price_paths(option) - } } impl OptionStrategy for MonteCarloModel {} diff --git a/src/options/types/asian_option.rs b/src/options/types/asian_option.rs index 2a949ea..bcc45bd 100644 --- a/src/options/types/asian_option.rs +++ b/src/options/types/asian_option.rs @@ -88,7 +88,7 @@ impl AsianOption { time_to_maturity, option_type, Permutation::Floating, - ) // strike is not used for floating + ) } } @@ -133,7 +133,6 @@ impl Option for AsianOption { OptionType::Put => (self.strike - avg_price).max(0.0), }, Permutation::Floating => match self.option_type { - // spot is the price at maturity OptionType::Call => (self.instrument.terminal_spot() - avg_price).max(0.0), OptionType::Put => (avg_price - self.instrument.terminal_spot()).max(0.0), }, diff --git a/src/options/types/lookback_option.rs b/src/options/types/lookback_option.rs index 939ec6a..a945868 100644 --- a/src/options/types/lookback_option.rs +++ b/src/options/types/lookback_option.rs @@ -4,6 +4,7 @@ use crate::options::{types::Permutation, Instrument, Option, OptionStyle, Option pub struct LookbackOption { pub instrument: Instrument, pub time_to_maturity: f64, + pub strike: f64, pub option_type: OptionType, pub option_style: OptionStyle, pub lookback_type: Permutation, @@ -13,6 +14,7 @@ impl LookbackOption { /// Create a new `LookbackOption`. pub fn new( instrument: Instrument, + strike: f64, time_to_maturity: f64, option_type: OptionType, lookback_type: Permutation, @@ -20,6 +22,7 @@ impl LookbackOption { Self { instrument, time_to_maturity, + strike, option_type, option_style: OptionStyle::Lookback(lookback_type), lookback_type, @@ -27,13 +30,34 @@ impl LookbackOption { } /// Create a new `Fixed` lookback option. - pub fn fixed(instrument: Instrument, ttm: f64, option_type: OptionType) -> Self { - Self::new(instrument, ttm, option_type, Permutation::Fixed) + pub fn fixed( + instrument: Instrument, + strike: f64, + time_to_maturity: f64, + option_type: OptionType, + ) -> Self { + Self::new( + instrument, + strike, + time_to_maturity, + option_type, + Permutation::Fixed, + ) } /// Create a new `Floating` lookback option. - pub fn floating(instrument: Instrument, ttm: f64, option_type: OptionType) -> Self { - Self::new(instrument, ttm, option_type, Permutation::Floating) + pub fn floating( + instrument: Instrument, + time_to_maturity: f64, + option_type: OptionType, + ) -> Self { + Self::new( + instrument, + 0.0, + time_to_maturity, + option_type, + Permutation::Floating, + ) } } @@ -59,7 +83,7 @@ impl Option for LookbackOption { } fn strike(&self) -> f64 { - self.instrument.spot() + self.strike } fn option_type(&self) -> OptionType { @@ -70,16 +94,22 @@ impl Option for LookbackOption { &self.option_style } - fn payoff(&self, spot: std::option::Option) -> f64 { - let spot_price = spot.unwrap_or(self.instrument.spot()); + 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 { Permutation::Fixed => match self.option_type { - OptionType::Call => (spot_price - self.strike()).max(0.0), - OptionType::Put => (self.strike() - spot_price).max(0.0), + OptionType::Call => (self.instrument.max_spot() - self.strike).max(0.0), + OptionType::Put => (self.strike - self.instrument.min_spot()).max(0.0), }, Permutation::Floating => match self.option_type { - OptionType::Call => (spot_price - self.instrument.min_spot).max(0.0), - OptionType::Put => (self.instrument.max_spot - spot_price).max(0.0), + OptionType::Call => { + (self.instrument.terminal_spot() - self.instrument.min_spot()).max(0.0) + } + OptionType::Put => { + (self.instrument.max_spot() - self.instrument.terminal_spot()).max(0.0) + } }, } } @@ -91,6 +121,7 @@ impl Option for LookbackOption { }; LookbackOption::new( self.instrument.clone(), + self.strike, self.time_to_maturity, flipped_option_type, self.lookback_type, diff --git a/tests/options_pricing.rs b/tests/options_pricing.rs index db3bac7..b117a8b 100644 --- a/tests/options_pricing.rs +++ b/tests/options_pricing.rs @@ -664,6 +664,47 @@ mod black_scholes_tests { } } + mod lookback_option_tests { + use super::*; + + #[test] + fn test_basis() { + let instrument = Instrument::new().with_spots(vec![100.0, 90.0, 110.0, 95.0]); + let option = LookbackOption::floating(instrument, 1.0, OptionType::Call); + let model = BlackScholesModel::new(0.1, 0.3); + + let price = model.price(&option); + assert_abs_diff_eq!(price, 27.382, epsilon = 0.0001); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 21.6149, epsilon = 0.0001); + } + + #[test] + fn test_edge() { + let option = + LookbackOption::floating(Instrument::new().with_spot(100.0), 1.0, OptionType::Call); + let model = BlackScholesModel::new(0.05, 0.2); + + let price = model.price(&option); + assert_abs_diff_eq!(price, 17.2168, epsilon = 0.0001); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 14.2905, epsilon = 0.0001); + + let result = std::panic::catch_unwind(|| { + let option = LookbackOption::floating( + Instrument::new().with_spot(0.0), + 0.0, + OptionType::Call, + ); + let model = BlackScholesModel::new(0.0, 0.0); + let _ = model.price(&option); + }); + assert!(result.is_err(), "Expected panic for zero spot price"); + } + } + #[test] fn test_black_scholes_iv() { let option = EuropeanOption::new( @@ -1177,6 +1218,86 @@ mod monte_carlo_tests { } } + mod lookback_option_tests { + use super::*; + + #[test] + fn test_avg() { + let instrument = Instrument::new() + .with_spot(30.0) + .with_continuous_dividend_yield(0.02); + let option = LookbackOption::floating(instrument, 1.0, OptionType::Call); + let arithmetic_model = MonteCarloModel::arithmetic(0.08, 0.3, 4_000, 20); + let geometric_model = MonteCarloModel::geometric(0.08, 0.3, 4_000, 20); + + let sd = 1.0; + + let price = arithmetic_model.price(&option); + assert_abs_diff_eq!(price, 6.636, epsilon = sd); + + let price = arithmetic_model.price(&option.flip()); + assert_abs_diff_eq!(price, 5.304, epsilon = sd); + + let price = geometric_model.price(&option); + assert_abs_diff_eq!(price, 6.636, epsilon = sd); + + let price = geometric_model.price(&option.flip()); + assert_abs_diff_eq!(price, 5.304, epsilon = sd); + } + + #[test] + fn test_fixed_itm() { + let instrument = Instrument::new().with_spot(110.0); + let option = LookbackOption::fixed(instrument, 31.0, 1.0, OptionType::Call); + let model = MonteCarloModel::geometric(0.03, 0.3, 4_000, 20); + + let price = model.price(&option); + assert_abs_diff_eq!(price, 101.0136, epsilon = 1.5); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 0.0, epsilon = 1.5); + } + + #[test] + fn test_fixed_otm() { + let instrument = Instrument::new().with_spot(15.0); + let option = LookbackOption::fixed(instrument, 20.0, 1.0, OptionType::Call); + let model = MonteCarloModel::arithmetic(0.01, 0.1, 4_000, 20); + + let price = model.price(&option); + assert_abs_diff_eq!(price, 0.0007, epsilon = 0.1); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 5.8520, epsilon = 0.1); + } + + #[test] + fn test_floating_itm() { + let instrument = Instrument::new().with_spot(110.0); + let option = LookbackOption::floating(instrument, 1.0, OptionType::Call); + let model = MonteCarloModel::geometric(0.03, 0.3, 4_000, 20); + + let price = model.price(&option); + assert_abs_diff_eq!(price, 22.225, epsilon = 1.5); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 21.917, epsilon = 1.5); + } + + #[test] + fn test_floating_otm() { + let instrument = Instrument::new().with_spot(15.0); + let option = LookbackOption::floating(instrument, 1.0, OptionType::Call); + let model = MonteCarloModel::arithmetic(0.01, 0.1, 4_000, 20); + + let price = model.price(&option); + assert_abs_diff_eq!(price, 1.068, epsilon = 0.1); + + let price = model.price(&option.flip()); + assert_abs_diff_eq!(price, 0.965, epsilon = 0.1); + } + } + mod binary_option_tests { use super::*; @@ -1671,7 +1792,7 @@ mod instrument_tests { // Average price should be 100.0 let rand_price = prices.iter().sum::() / prices.len() as f64; - assert_abs_diff_eq!(rand_price, 100f64, epsilon = 100.0 * 0.25); + assert_abs_diff_eq!(rand_price, 100f64, epsilon = 30.0); } #[test] @@ -1801,9 +1922,25 @@ mod option_trait_tests { OptionType::Put, ); assert_implements_option_trait(&opt); - let opt = LookbackOption::fixed(Instrument::new().with_spot(100.0), 1.0, OptionType::Call); + let opt = LookbackOption::fixed( + Instrument::new().with_spot(100.0), + 99.0, + 1.0, + OptionType::Call, + ); + assert_implements_option_trait(&opt); + let opt = LookbackOption::fixed( + Instrument::new().with_spot(100.0), + 99.0, + 1.0, + OptionType::Put, + ); + assert_implements_option_trait(&opt); + let opt = + LookbackOption::floating(Instrument::new().with_spot(100.0), 1.0, OptionType::Call); assert_implements_option_trait(&opt); - let opt = LookbackOption::fixed(Instrument::new().with_spot(100.0), 1.0, OptionType::Put); + let opt = + LookbackOption::floating(Instrument::new().with_spot(100.0), 1.0, OptionType::Put); assert_implements_option_trait(&opt); let opt = BinaryOption::cash_or_nothing( Instrument::new().with_spot(100.0),