From bd74febef46b3ae8ec27d9c460a9ef816f7ebe28 Mon Sep 17 00:00:00 2001 From: Yasser Naji Date: Tue, 18 Mar 2025 23:01:44 +0000 Subject: [PATCH 01/11] Longstaff-Schwartz: Define Struct and imports --- .../src/options/longstaff_schwartz.rs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 crates/RustQuant_instruments/src/options/longstaff_schwartz.rs diff --git a/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs b/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs new file mode 100644 index 00000000..2de175cd --- /dev/null +++ b/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs @@ -0,0 +1,28 @@ +use time::Date; +use nalgebra::{DMatrix, DVector}; +use rand::thread_rng; +use rand_distr::{Normal, Distribution}; +use RustQuant_time::{today, DayCountConvention}; +use crate::option_flags::TypeFlag; + +/// Longstaff-Schwartz Option pricing model. +pub struct LongstaffScwhartzPricer { + /// Spot Price + pub initial_price: f64, + /// Strike price + pub strike_price: f64, + /// Risk free rate + pub risk_free_rate: f64, + /// Volatility + pub volatility: f64, + /// Evaluation date + pub evaluation_date: Option, + /// Maturity date + pub expiration_date: Date, + /// Time steps + pub time_steps: u32, + /// Option Type + pub type_flag: TypeFlag, + /// Number of simulations + pub num_simulations: u64 +} From 4e17ea2afbcbe1189edec4928f4637efb1fe4bb8 Mon Sep 17 00:00:00 2001 From: Yasser Naji Date: Tue, 18 Mar 2025 23:04:07 +0000 Subject: [PATCH 02/11] Longstaff-Schwartz: Define constructor, helper and pricer functions --- .../src/options/longstaff_schwartz.rs | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) diff --git a/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs b/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs index 2de175cd..aabdaf95 100644 --- a/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs +++ b/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs @@ -26,3 +26,193 @@ pub struct LongstaffScwhartzPricer { /// Number of simulations pub num_simulations: u64 } + +impl LongstaffScwhartzPricer { + + /// Constructor for LongstaffScwhartz. + #[allow(clippy::too_many_arguments)] + pub fn new( + initial_price: f64, + strike_price: f64, + risk_free_rate: f64, + volatility: f64, + evaluation_date: Option, + expiration_date: Date, + time_steps: u32, + type_flag: TypeFlag, + num_simulations: u64, + ) -> Self { + assert!(evaluation_date.unwrap_or(today()) < expiration_date); + assert!(initial_price > 0.0, "initial_price must be positive!"); + assert!(strike_price > 0.0, "strike_price must be positive!"); + assert!(risk_free_rate > 0.0, "risk_free_rate must be positive!"); + assert!(volatility > 0.0, "volatility must be positive!"); + assert!(time_steps > 0, "time_steps must be positive!"); + assert!(num_simulations > 0, "num_simulations must be positive!"); + + Self { + initial_price, + strike_price, + risk_free_rate, + volatility, + evaluation_date, + expiration_date, + time_steps, + type_flag, + num_simulations + } + } + + /// Run Longstaff-Schwartz pricing method for American options. + pub fn generate_price(&self) -> f64 { + let mut current_time; + let end_time: f64 = self.year_fraction(); + let delta_t: f64 = end_time / self.time_steps as f64; + let mut markov_chain: Vec = self.generate_end_points(end_time); + let mut asset_prices: Vec = self.calculate_asset_prices(&markov_chain); + let mut payoffs: Vec = asset_prices.iter().map(|asset_price| self.calculate_payoff(asset_price)).collect(); + let mut regression_index: i32; + + for time_step in (1..self.time_steps).rev() { + current_time = (time_step as f64) * delta_t; + markov_chain = self.backwards_time_induction( + markov_chain, delta_t, current_time + ); + asset_prices = self.calculate_asset_prices(&markov_chain); + let (in_the_money_indices, in_the_money_assets) = self.in_the_money_assets(&asset_prices); + + let filter_in_the_money_payoffs = payoffs.iter().enumerate().filter_map(|(i, payoff)| { + if in_the_money_indices.contains(&i) { + Some(payoff.clone()) + } else { + None + } + }).collect(); + + let laguerre_matrix = self.create_laguerre_matrix(&in_the_money_assets); + let in_the_money_payoffs = self.discount(delta_t) * DVector::from_vec(filter_in_the_money_payoffs); + let laguerre_matrix_transpose = laguerre_matrix.transpose(); + + let least_squares_calculation = (&laguerre_matrix_transpose * &laguerre_matrix) + .qr() + .solve(&(&laguerre_matrix_transpose * in_the_money_payoffs)); + + match least_squares_calculation { + Some(regression_coefficients) => { + let continuation_value = &laguerre_matrix * regression_coefficients; + regression_index = -1; + for i in 0..self.num_simulations { + if in_the_money_indices.contains(&(i as usize)) { + regression_index += 1; + let payoff_at_current_time: f64 = self.calculate_payoff(&asset_prices[i as usize]); + payoffs[i as usize] = if payoff_at_current_time > continuation_value[regression_index as usize] { + payoff_at_current_time + } else { + self.discount(delta_t) * payoffs[i as usize] + } + } else { + payoffs[i as usize] = self.discount(delta_t) * payoffs[i as usize]; + } + } + }, + None => { + for i in 0..self.num_simulations { + payoffs[i as usize] = self.discount(delta_t) * payoffs[i as usize]; + } + } + } + } + payoffs.iter().sum::() / self.num_simulations as f64 + } + + fn create_laguerre_matrix(&self, in_the_money_assets: &[f64]) -> DMatrix { + let mut laguerre_matrix = DMatrix::zeros( + in_the_money_assets.len(), 5 as usize + ); + for i in 0..in_the_money_assets.len() { + for j in 0..5 { + laguerre_matrix[(i as usize, j as usize)] = match j { + 0 => 1.0, + 1 => 1.0 - in_the_money_assets[i as usize], + _ => (((2 * (j - 1)) as f64 + + 1.0 - in_the_money_assets[i as usize]) + * laguerre_matrix[(i as usize, (j - 1) as usize)] + - ((j - 1) as f64) + * laguerre_matrix[(i as usize, (j - 2) as usize)]) + / (j as f64), + }; + } + } + laguerre_matrix + } + + fn discount(&self, delta_t: f64) -> f64 { + f64::exp(- self.risk_free_rate * delta_t) + } + + fn generate_end_points(&self, end_time: f64) -> Vec { + let mut rng: rand::prelude::ThreadRng = thread_rng(); + let normal_distribution: Normal = Normal::new(0.0, 1.0).unwrap(); + let mut markov_chain: Vec = vec![]; + for _ in 0..self.num_simulations { + markov_chain.push( + ((self.risk_free_rate - 0.5 * self.volatility * self.volatility) * end_time) + + self.volatility * end_time.sqrt() * normal_distribution.sample(&mut rng)) + } + markov_chain + } + + fn calculate_asset_prices(&self, markov_chain: &[f64]) -> Vec { + let mut asset_prices = vec![]; + for i in 0..self.num_simulations { + asset_prices.push(self.initial_price * f64::exp(markov_chain[i as usize])) + } + asset_prices + } + + fn calculate_payoff(&self, asset_price: &f64) -> f64 { + + match self.type_flag { + TypeFlag::Call => { + (asset_price - self.strike_price).max(0.0) + }, + TypeFlag::Put => { + (self.strike_price - asset_price).max(0.0) + } + } + } + + fn backwards_time_induction(&self, mut markov_chain: Vec, delta_t: f64, time: f64) -> Vec { + + let mut rng: rand::prelude::ThreadRng = thread_rng(); + let normal_distribution: Normal = Normal::new(0.0, 1.0).unwrap(); + + for i in 0..self.num_simulations { + markov_chain[i as usize] = (markov_chain[i as usize] * time / (time + delta_t)) + + self.volatility * (time * delta_t / (time + delta_t)).sqrt() * normal_distribution.sample(&mut rng) + } + markov_chain + } + + fn year_fraction(&self) -> f64 { + + DayCountConvention::default().day_count_factor( + self.evaluation_date.unwrap_or(today()), + self.expiration_date, + ) + } + + fn in_the_money_assets(&self, asset_prices: &[f64]) -> (Vec, Vec) { + let mut in_the_money_indices: Vec = vec![]; + + let in_the_money_assets = asset_prices.iter().enumerate().filter_map( + |(i, asset_price)| { + let payoff = self.calculate_payoff(asset_price); + if payoff > 0.0 { + in_the_money_indices.push(i); + Some(payoff) + } else { None } } + ).collect(); + (in_the_money_indices, in_the_money_assets) + } +} From 4b6f731db8ff64532ed681b4ec48b65525694811 Mon Sep 17 00:00:00 2001 From: Yasser Naji Date: Tue, 18 Mar 2025 23:04:42 +0000 Subject: [PATCH 03/11] Longstaff-Schwartz: Unit tests --- .../src/options/longstaff_schwartz.rs | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs b/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs index aabdaf95..e4c11002 100644 --- a/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs +++ b/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs @@ -216,3 +216,155 @@ impl LongstaffScwhartzPricer { (in_the_money_indices, in_the_money_assets) } } + +#[cfg(test)] +mod tests_longstaff_schwartz_pricer_at_the_money { + use super::*; + use time::macros::date; + + const TOLERANCE: f64 = 0.25; + const ATM_CALL_EXPECTED_PRICE: f64 = 0.680; + const ATM_PUT_EXPECTED_PRICE: f64 = 0.243; + + #[test] + fn test_longstaff_schwartz_call_at_the_money() { + let longstaff_schwartz_pricer = LongstaffScwhartzPricer::new( + 10.0, + 10.0, + 0.05, + 0.1, + Some(date!(2024 - 01 - 01)), + date!(2025 - 01 - 01), + 5000, + TypeFlag::Call, + 600 + ); + + assert!( + (longstaff_schwartz_pricer.generate_price() - ATM_CALL_EXPECTED_PRICE).abs() < TOLERANCE + ); + } + + #[test] + fn test_longstaff_schwartz_put_at_the_money() { + let longstaff_schwartz_pricer = LongstaffScwhartzPricer::new( + 10.0, + 10.0, + 0.05, + 0.1, + Some(date!(2024 - 01 - 01)), + date!(2025 - 01 - 01), + 5000, + TypeFlag::Put, + 600 + ); + + assert!( + (longstaff_schwartz_pricer.generate_price() - ATM_PUT_EXPECTED_PRICE).abs() < TOLERANCE + ); + } +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// UNIT TESTS: IN THE MONEY +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +#[cfg(test)] +mod tests_longstaff_schwartz_pricer_in_the_money { + use super::*; + use time::macros::date; + + const TOLERANCE: f64 = 0.25; + const ITM_CALL_EXPECTED_PRICE: f64 = 5.4889; + const ITM_PUT_EXPECTED_PRICE: f64 = 5.0000; + + #[test] + fn test_longstaff_schwartz_call_in_the_money() { + let longstaff_schwartz_pricer = LongstaffScwhartzPricer::new( + 15.0, + 10.0, + 0.05, + 0.1, + Some(date!(2024 - 01 - 01)), + date!(2025 - 01 - 01), + 5000, + TypeFlag::Call, + 600 + ); + + assert!( + (longstaff_schwartz_pricer.generate_price() - ITM_CALL_EXPECTED_PRICE).abs() < TOLERANCE + ); + } + + #[test] + fn test_longstaff_schwartz_put_in_the_money() { + let longstaff_schwartz_pricer = LongstaffScwhartzPricer::new( + 10.0, + 15.0, + 0.05, + 0.1, + Some(date!(2024 - 01 - 01)), + date!(2025 - 01 - 01), + 5000, + TypeFlag::Put, + 600 + ); + + assert!( + (longstaff_schwartz_pricer.generate_price() - ITM_PUT_EXPECTED_PRICE).abs() < TOLERANCE + ); + } +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// UNIT TESTS: OUT OF THE MONEY +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +#[cfg(test)] +mod tests_longstaff_schwartz_pricer_out_the_money { + use super::*; + use time::macros::date; + + const TOLERANCE: f64 = 0.25; + const OTM_CALL_EXPECTED_PRICE: f64 = 0.0000; + const OTM_PUT_EXPECTED_PRICE: f64 = 0.0000; + + #[test] + fn test_longstaff_schwartz_call_out_the_money() { + let longstaff_schwartz_pricer = LongstaffScwhartzPricer::new( + 10.0, + 15.0, + 0.05, + 0.1, + Some(date!(2024 - 01 - 01)), + date!(2025 - 01 - 01), + 5000, + TypeFlag::Call, + 600 + ); + + assert!( + (longstaff_schwartz_pricer.generate_price() - OTM_CALL_EXPECTED_PRICE).abs() < TOLERANCE + ); + } + + #[test] + fn test_longstaff_schwartz_put_out_the_money() { + let longstaff_schwartz_pricer = LongstaffScwhartzPricer::new( + 15.0, + 10.0, + 0.05, + 0.1, + Some(date!(2024 - 01 - 01)), + date!(2025 - 01 - 01), + 5000, + TypeFlag::Put, + 600 + ); + + assert!( + (longstaff_schwartz_pricer.generate_price() - OTM_PUT_EXPECTED_PRICE).abs() < TOLERANCE + ); + } +} From 4b9fa6aec1767db0f2ba0f1a115044244584da0f Mon Sep 17 00:00:00 2001 From: Yasser Naji Date: Tue, 18 Mar 2025 23:12:00 +0000 Subject: [PATCH 04/11] Longstaff-Schwartz: Add required packages in Cargo.toml --- crates/RustQuant_instruments/Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/RustQuant_instruments/Cargo.toml b/crates/RustQuant_instruments/Cargo.toml index 58b5891c..5ac267d0 100644 --- a/crates/RustQuant_instruments/Cargo.toml +++ b/crates/RustQuant_instruments/Cargo.toml @@ -32,6 +32,9 @@ derive_builder = { workspace = true } errorfunctions = { workspace = true } serde = { workspace = true } num = { workspace = true } +nalgebra = { workspace = true } +rand = { workspace = true } +rand_distr = { workspace = true } ## ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## RUSTDOC CONFIGURATION From 60c71f0b28d3e19bb954b14ed825374dad313160 Mon Sep 17 00:00:00 2001 From: Yasser Naji Date: Tue, 18 Mar 2025 23:12:49 +0000 Subject: [PATCH 05/11] Longstaff-Schwartz: Make longstaff_schwartz module public --- crates/RustQuant_instruments/src/options/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/RustQuant_instruments/src/options/mod.rs b/crates/RustQuant_instruments/src/options/mod.rs index 8affb860..016a9d76 100644 --- a/crates/RustQuant_instruments/src/options/mod.rs +++ b/crates/RustQuant_instruments/src/options/mod.rs @@ -79,3 +79,7 @@ pub use supershare::*; /// Log contracts and options. pub mod log; pub use log::*; + +/// Longstaff-Schwartz pricer. +pub mod longstaff_schwartz; +pub use longstaff_schwartz::*; From f575f4a5e8f0acc330cd9c39ea2c4add936d025b Mon Sep 17 00:00:00 2001 From: Yasser Naji Date: Wed, 19 Mar 2025 14:16:01 +0000 Subject: [PATCH 06/11] Longstaff-Schwartz: Update pricer with seeding capability --- .../src/options/longstaff_schwartz.rs | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs b/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs index e4c11002..8f0d3ee9 100644 --- a/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs +++ b/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs @@ -1,6 +1,6 @@ use time::Date; use nalgebra::{DMatrix, DVector}; -use rand::thread_rng; +use rand::{rngs::StdRng, SeedableRng}; use rand_distr::{Normal, Distribution}; use RustQuant_time::{today, DayCountConvention}; use crate::option_flags::TypeFlag; @@ -24,7 +24,9 @@ pub struct LongstaffScwhartzPricer { /// Option Type pub type_flag: TypeFlag, /// Number of simulations - pub num_simulations: u64 + pub num_simulations: u64, + /// Seed + pub seed: Option } impl LongstaffScwhartzPricer { @@ -41,6 +43,7 @@ impl LongstaffScwhartzPricer { time_steps: u32, type_flag: TypeFlag, num_simulations: u64, + seed: Option ) -> Self { assert!(evaluation_date.unwrap_or(today()) < expiration_date); assert!(initial_price > 0.0, "initial_price must be positive!"); @@ -59,13 +62,14 @@ impl LongstaffScwhartzPricer { expiration_date, time_steps, type_flag, - num_simulations + num_simulations, + seed } } /// Run Longstaff-Schwartz pricing method for American options. pub fn generate_price(&self) -> f64 { - let mut current_time; + // let mut current_time; let end_time: f64 = self.year_fraction(); let delta_t: f64 = end_time / self.time_steps as f64; let mut markov_chain: Vec = self.generate_end_points(end_time); @@ -74,9 +78,9 @@ impl LongstaffScwhartzPricer { let mut regression_index: i32; for time_step in (1..self.time_steps).rev() { - current_time = (time_step as f64) * delta_t; + // current_time = (time_step as f64) * delta_t; markov_chain = self.backwards_time_induction( - markov_chain, delta_t, current_time + markov_chain, delta_t, time_step ); asset_prices = self.calculate_asset_prices(&markov_chain); let (in_the_money_indices, in_the_money_assets) = self.in_the_money_assets(&asset_prices); @@ -151,7 +155,7 @@ impl LongstaffScwhartzPricer { } fn generate_end_points(&self, end_time: f64) -> Vec { - let mut rng: rand::prelude::ThreadRng = thread_rng(); + let mut rng = StdRng::seed_from_u64(self.seed.unwrap_or_else(rand::random)); let normal_distribution: Normal = Normal::new(0.0, 1.0).unwrap(); let mut markov_chain: Vec = vec![]; for _ in 0..self.num_simulations { @@ -182,14 +186,18 @@ impl LongstaffScwhartzPricer { } } - fn backwards_time_induction(&self, mut markov_chain: Vec, delta_t: f64, time: f64) -> Vec { + fn backwards_time_induction(&self, mut markov_chain: Vec, delta_t: f64, time_step: u32) -> Vec { - let mut rng: rand::prelude::ThreadRng = thread_rng(); + let mut rng = match self.seed { + Some(seed) => StdRng::seed_from_u64(seed.wrapping_add(time_step as u64)), + None => StdRng::seed_from_u64(rand::random()) + }; let normal_distribution: Normal = Normal::new(0.0, 1.0).unwrap(); + let current_time: f64 = (time_step as f64) * delta_t; for i in 0..self.num_simulations { - markov_chain[i as usize] = (markov_chain[i as usize] * time / (time + delta_t)) - + self.volatility * (time * delta_t / (time + delta_t)).sqrt() * normal_distribution.sample(&mut rng) + markov_chain[i as usize] = (markov_chain[i as usize] * current_time / (current_time + delta_t)) + + self.volatility * (current_time * delta_t / (current_time + delta_t)).sqrt() * normal_distribution.sample(&mut rng) } markov_chain } From 552512e8a7560be59516d61408d83e9e726b43e6 Mon Sep 17 00:00:00 2001 From: Yasser Naji Date: Wed, 19 Mar 2025 14:17:43 +0000 Subject: [PATCH 07/11] Longstaff-Schwartz: Update unit tests + Add unit tests for seeding --- .../src/options/longstaff_schwartz.rs | 82 ++++++++++++++++--- 1 file changed, 69 insertions(+), 13 deletions(-) diff --git a/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs b/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs index 8f0d3ee9..fbcb27d7 100644 --- a/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs +++ b/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs @@ -243,9 +243,10 @@ mod tests_longstaff_schwartz_pricer_at_the_money { 0.1, Some(date!(2024 - 01 - 01)), date!(2025 - 01 - 01), - 5000, + 1000, TypeFlag::Call, - 600 + 500, + None ); assert!( @@ -262,9 +263,10 @@ mod tests_longstaff_schwartz_pricer_at_the_money { 0.1, Some(date!(2024 - 01 - 01)), date!(2025 - 01 - 01), - 5000, + 1000, TypeFlag::Put, - 600 + 500, + None ); assert!( @@ -295,11 +297,12 @@ mod tests_longstaff_schwartz_pricer_in_the_money { 0.1, Some(date!(2024 - 01 - 01)), date!(2025 - 01 - 01), - 5000, + 1000, TypeFlag::Call, - 600 + 500, + None ); - + assert!( (longstaff_schwartz_pricer.generate_price() - ITM_CALL_EXPECTED_PRICE).abs() < TOLERANCE ); @@ -314,9 +317,10 @@ mod tests_longstaff_schwartz_pricer_in_the_money { 0.1, Some(date!(2024 - 01 - 01)), date!(2025 - 01 - 01), - 5000, + 1000, TypeFlag::Put, - 600 + 500, + None ); assert!( @@ -347,9 +351,10 @@ mod tests_longstaff_schwartz_pricer_out_the_money { 0.1, Some(date!(2024 - 01 - 01)), date!(2025 - 01 - 01), - 5000, + 1000, TypeFlag::Call, - 600 + 500, + None ); assert!( @@ -366,9 +371,10 @@ mod tests_longstaff_schwartz_pricer_out_the_money { 0.1, Some(date!(2024 - 01 - 01)), date!(2025 - 01 - 01), - 5000, + 1000, TypeFlag::Put, - 600 + 500, + None ); assert!( @@ -376,3 +382,53 @@ mod tests_longstaff_schwartz_pricer_out_the_money { ); } } + +#[cfg(test)] +mod tests_longstaff_schwartz_pricer_seeded { + use super::*; + use time::macros::date; + + const TOLERANCE: f64 = 0.25; + const CALL_SEEDED_EXPECTED_PRICE: f64 = 5.4889; + const PUT_SEEDED_PUT_EXPECTED_PRICE: f64 = 0.0000; + + #[test] + fn test_longstaff_schwartz_call_seeded() { + let longstaff_schwartz_pricer = LongstaffScwhartzPricer::new( + 15.0, + 10.0, + 0.05, + 0.1, + Some(date!(2024 - 01 - 01)), + date!(2025 - 01 - 01), + 1000, + TypeFlag::Call, + 500, + Some(1234) + ); + + assert!( + (longstaff_schwartz_pricer.generate_price() - CALL_SEEDED_EXPECTED_PRICE).abs() < TOLERANCE + ); + } + + #[test] + fn test_longstaff_schwartz_put_seeded() { + let longstaff_schwartz_pricer = LongstaffScwhartzPricer::new( + 15.0, + 10.0, + 0.05, + 0.1, + Some(date!(2024 - 01 - 01)), + date!(2025 - 01 - 01), + 1000, + TypeFlag::Put, + 500, + Some(9876) + ); + + assert!( + (longstaff_schwartz_pricer.generate_price() - PUT_SEEDED_PUT_EXPECTED_PRICE).abs() < TOLERANCE + ); + } +} From bdcdf57f56dd7282dfddc7c2f38c19c336399487 Mon Sep 17 00:00:00 2001 From: Yasser Naji Date: Wed, 19 Mar 2025 23:08:25 +0000 Subject: [PATCH 08/11] Longstaff-Schwartz: Assertions on dates + remove comments --- .../RustQuant_instruments/src/options/longstaff_schwartz.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs b/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs index fbcb27d7..ceffdda4 100644 --- a/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs +++ b/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs @@ -45,7 +45,7 @@ impl LongstaffScwhartzPricer { num_simulations: u64, seed: Option ) -> Self { - assert!(evaluation_date.unwrap_or(today()) < expiration_date); + assert!(evaluation_date.unwrap_or(today()) < expiration_date, "expiration_date must be after evaluation_date!"); assert!(initial_price > 0.0, "initial_price must be positive!"); assert!(strike_price > 0.0, "strike_price must be positive!"); assert!(risk_free_rate > 0.0, "risk_free_rate must be positive!"); @@ -69,7 +69,6 @@ impl LongstaffScwhartzPricer { /// Run Longstaff-Schwartz pricing method for American options. pub fn generate_price(&self) -> f64 { - // let mut current_time; let end_time: f64 = self.year_fraction(); let delta_t: f64 = end_time / self.time_steps as f64; let mut markov_chain: Vec = self.generate_end_points(end_time); @@ -78,7 +77,7 @@ impl LongstaffScwhartzPricer { let mut regression_index: i32; for time_step in (1..self.time_steps).rev() { - // current_time = (time_step as f64) * delta_t; + markov_chain = self.backwards_time_induction( markov_chain, delta_t, time_step ); From bf84163145b813fac55fb4f19903fcb5a650e7d6 Mon Sep 17 00:00:00 2001 From: Yasser Naji Date: Thu, 20 Mar 2025 09:02:16 +0000 Subject: [PATCH 09/11] Longstaff-Schwartz: Header for at-the-money unit tests --- .../RustQuant_instruments/src/options/longstaff_schwartz.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs b/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs index ceffdda4..9452e7be 100644 --- a/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs +++ b/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs @@ -224,6 +224,10 @@ impl LongstaffScwhartzPricer { } } +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// UNIT TESTS: AT THE MONEY +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + #[cfg(test)] mod tests_longstaff_schwartz_pricer_at_the_money { use super::*; From d66a22cf7599f0a3aec02ec3f1a15725aa491b2c Mon Sep 17 00:00:00 2001 From: Yasser Naji Date: Thu, 20 Mar 2025 09:21:01 +0000 Subject: [PATCH 10/11] Longstaff-Schwartz: More robust unit tests for seedable cases --- .../src/options/longstaff_schwartz.rs | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs b/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs index 9452e7be..ec47361c 100644 --- a/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs +++ b/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs @@ -386,14 +386,17 @@ mod tests_longstaff_schwartz_pricer_out_the_money { } } +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// UNIT TESTS: SEED +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + #[cfg(test)] mod tests_longstaff_schwartz_pricer_seeded { use super::*; use time::macros::date; - const TOLERANCE: f64 = 0.25; - const CALL_SEEDED_EXPECTED_PRICE: f64 = 5.4889; - const PUT_SEEDED_PUT_EXPECTED_PRICE: f64 = 0.0000; + const CALL_SEEDED_EXPECTED_PRICE: f64 = 5.575_013_446_249_429; + const PUT_SEEDED_EXPECTED_PRICE: f64 = 5.009_667_419_662_556; #[test] fn test_longstaff_schwartz_call_seeded() { @@ -409,17 +412,15 @@ mod tests_longstaff_schwartz_pricer_seeded { 500, Some(1234) ); - - assert!( - (longstaff_schwartz_pricer.generate_price() - CALL_SEEDED_EXPECTED_PRICE).abs() < TOLERANCE - ); + + assert!(longstaff_schwartz_pricer.generate_price() == CALL_SEEDED_EXPECTED_PRICE); } #[test] fn test_longstaff_schwartz_put_seeded() { let longstaff_schwartz_pricer = LongstaffScwhartzPricer::new( - 15.0, 10.0, + 15.0, 0.05, 0.1, Some(date!(2024 - 01 - 01)), @@ -430,8 +431,6 @@ mod tests_longstaff_schwartz_pricer_seeded { Some(9876) ); - assert!( - (longstaff_schwartz_pricer.generate_price() - PUT_SEEDED_PUT_EXPECTED_PRICE).abs() < TOLERANCE - ); + assert!(longstaff_schwartz_pricer.generate_price() == PUT_SEEDED_EXPECTED_PRICE); } } From 37cab3f2548822db838613bd25ef7378d8c77085 Mon Sep 17 00:00:00 2001 From: Yasser Naji Date: Fri, 28 Mar 2025 20:43:19 +0000 Subject: [PATCH 11/11] Longstaff-Schwartz: Utilise assert_approx_equal for unit tests + lower tolerance --- .../src/options/longstaff_schwartz.rs | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs b/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs index ec47361c..75f28205 100644 --- a/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs +++ b/crates/RustQuant_instruments/src/options/longstaff_schwartz.rs @@ -232,8 +232,9 @@ impl LongstaffScwhartzPricer { mod tests_longstaff_schwartz_pricer_at_the_money { use super::*; use time::macros::date; + use RustQuant_utils::assert_approx_equal; - const TOLERANCE: f64 = 0.25; + const TOLERANCE: f64 = 0.125; const ATM_CALL_EXPECTED_PRICE: f64 = 0.680; const ATM_PUT_EXPECTED_PRICE: f64 = 0.243; @@ -252,8 +253,10 @@ mod tests_longstaff_schwartz_pricer_at_the_money { None ); - assert!( - (longstaff_schwartz_pricer.generate_price() - ATM_CALL_EXPECTED_PRICE).abs() < TOLERANCE + assert_approx_equal!( + longstaff_schwartz_pricer.generate_price(), + ATM_CALL_EXPECTED_PRICE, + TOLERANCE ); } @@ -272,8 +275,10 @@ mod tests_longstaff_schwartz_pricer_at_the_money { None ); - assert!( - (longstaff_schwartz_pricer.generate_price() - ATM_PUT_EXPECTED_PRICE).abs() < TOLERANCE + assert_approx_equal!( + longstaff_schwartz_pricer.generate_price(), + ATM_PUT_EXPECTED_PRICE, + TOLERANCE ); } } @@ -286,8 +291,9 @@ mod tests_longstaff_schwartz_pricer_at_the_money { mod tests_longstaff_schwartz_pricer_in_the_money { use super::*; use time::macros::date; + use RustQuant_utils::assert_approx_equal; - const TOLERANCE: f64 = 0.25; + const TOLERANCE: f64 = 0.125; const ITM_CALL_EXPECTED_PRICE: f64 = 5.4889; const ITM_PUT_EXPECTED_PRICE: f64 = 5.0000; @@ -305,9 +311,10 @@ mod tests_longstaff_schwartz_pricer_in_the_money { 500, None ); - - assert!( - (longstaff_schwartz_pricer.generate_price() - ITM_CALL_EXPECTED_PRICE).abs() < TOLERANCE + assert_approx_equal!( + longstaff_schwartz_pricer.generate_price(), + ITM_CALL_EXPECTED_PRICE, + TOLERANCE ); } @@ -326,8 +333,10 @@ mod tests_longstaff_schwartz_pricer_in_the_money { None ); - assert!( - (longstaff_schwartz_pricer.generate_price() - ITM_PUT_EXPECTED_PRICE).abs() < TOLERANCE + assert_approx_equal!( + longstaff_schwartz_pricer.generate_price(), + ITM_PUT_EXPECTED_PRICE, + TOLERANCE ); } } @@ -340,8 +349,9 @@ mod tests_longstaff_schwartz_pricer_in_the_money { mod tests_longstaff_schwartz_pricer_out_the_money { use super::*; use time::macros::date; + use RustQuant_utils::assert_approx_equal; - const TOLERANCE: f64 = 0.25; + const TOLERANCE: f64 = 0.125; const OTM_CALL_EXPECTED_PRICE: f64 = 0.0000; const OTM_PUT_EXPECTED_PRICE: f64 = 0.0000; @@ -360,8 +370,10 @@ mod tests_longstaff_schwartz_pricer_out_the_money { None ); - assert!( - (longstaff_schwartz_pricer.generate_price() - OTM_CALL_EXPECTED_PRICE).abs() < TOLERANCE + assert_approx_equal!( + longstaff_schwartz_pricer.generate_price(), + OTM_CALL_EXPECTED_PRICE, + TOLERANCE ); } @@ -380,8 +392,10 @@ mod tests_longstaff_schwartz_pricer_out_the_money { None ); - assert!( - (longstaff_schwartz_pricer.generate_price() - OTM_PUT_EXPECTED_PRICE).abs() < TOLERANCE + assert_approx_equal!( + longstaff_schwartz_pricer.generate_price(), + OTM_PUT_EXPECTED_PRICE, + TOLERANCE ); } }