From ff0f2c94131695ab0904c2afb6011d9ea2015e97 Mon Sep 17 00:00:00 2001 From: Joaquin Bejar Date: Fri, 17 Apr 2026 09:38:24 +0200 Subject: [PATCH 1/3] fix(pricing): correct underflow guard in TelegraphProcess::next_state (#351) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The branch `if lambda_dt < dec!(11.7)` was always true for non-negative lambda and dt, forcing probability = 1.0 every step and degenerating the chain into a deterministic alternation. Correct guard is `if lambda_dt < dec!(-11.7)` (underflow at very-negative exponent), with the normal branch computing 1 - exp(-lambda * dt) per the module docs. Adds a Monte-Carlo regression test that asserts the empirical flip rate at (lambda=0.5, dt=0.01) matches 1 - exp(-0.005) ≈ 0.00499 within 5σ over 100k samples. --- src/pricing/telegraph.rs | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/pricing/telegraph.rs b/src/pricing/telegraph.rs index 5b0e6cef..06becd2a 100644 --- a/src/pricing/telegraph.rs +++ b/src/pricing/telegraph.rs @@ -152,7 +152,11 @@ impl TelegraphProcess { self.lambda_up }; let lambda_dt = -lambda * dt; - let probability = if lambda_dt < dec!(11.7) { + // lambda_dt is non-positive (lambda, dt >= 0). For very-negative + // values exp(lambda_dt) underflows to 0; treat as a guaranteed + // flip (probability = 1). Otherwise use the standard Poisson + // transition: P(flip in dt) = 1 - exp(-lambda * dt). + let probability = if lambda_dt < dec!(-11.7) { Decimal::ONE } else { Decimal::ONE - lambda_dt.exp() @@ -412,6 +416,40 @@ mod tests_telegraph_process_basis { // There's a chance the state didn't change, so we can't assert inequality } + #[test] + fn test_next_state_empirical_flip_rate_matches_poisson() { + // Regression test for #351: prior code had an inverted underflow + // guard that forced probability = 1.0 every step. Verify the + // empirical flip rate now matches the Poisson transition + // probability P(flip in dt) = 1 - exp(-lambda * dt) within a + // 5 σ Monte-Carlo bound. + let lambda_f = 0.5_f64; + let dt_f = 0.01_f64; + let mut tp = TelegraphProcess::new(dec!(0.5), dec!(0.5)); + + let n = 100_000_u64; + let mut prev = tp.get_current_state(); + let mut flips: u64 = 0; + for _ in 0..n { + let next = tp.next_state(dec!(0.01)); + if next != prev { + flips += 1; + } + prev = next; + } + let empirical = flips as f64 / n as f64; + let expected = 1.0 - (-lambda_f * dt_f).exp(); + let std_err = (expected * (1.0 - expected) / n as f64).sqrt(); + + assert!( + (empirical - expected).abs() < 5.0 * std_err, + "empirical flip rate {empirical} differs from expected {expected} by more than 5σ ({})", + 5.0 * std_err + ); + // Sanity: must be far below 1.0 (the buggy value). + assert!(empirical < 0.05, "flip rate suspiciously high: {empirical}"); + } + #[test] fn test_telegraph_process_get_current_state() { let tp = TelegraphProcess::new(dec!(0.5), dec!(0.5)); From 61ea9c0260112e808edadefd2372d9a40673b576 Mon Sep 17 00:00:00 2001 From: Joaquin Bejar Date: Fri, 17 Apr 2026 12:21:05 +0200 Subject: [PATCH 2/3] refactor(simulation): make RandomWalk::new and Simulator::new fallible (#349) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both constructors now accept a fallible generator `F: Fn(&WalkParams) -> Result>, E>` and return `Result`, propagating any generator error unchanged. This removes the `|p| generator_positive(p).unwrap()` wrappers required since PR #348 made the chain generators fallible, restoring panic-free end-to-end pipelines per the M1 — Panic-Free Core milestone. Also adds `From for SimulationError` (symmetric with the existing reverse impl) so callers can `?`-propagate chain-generator errors through simulation contexts. Negative tests for both constructors verify the error surfaces unchanged and that `Simulator::new` short-circuits on the first failing generator invocation rather than building a partial simulator. All in-tree callers updated (strategy tests, simulator/randomwalk test mods, integration tests, examples). Test helpers using ad-hoc generators bumped to `Result<_, Infallible>` with `let-else` / `unreachable!()` patterns where appropriate. --- .../src/bin/historical_build_chain.rs | 8 +- .../src/bin/long_call_strategy_simulation.rs | 4 +- .../src/bin/position_simulator.rs | 9 +- .../src/bin/random_walk.rs | 4 +- .../src/bin/random_walk_build_chain.rs | 8 +- .../src/bin/random_walk_build_series.rs | 8 +- .../src/bin/random_walk_chain.rs | 8 +- .../src/bin/short_put_simulation.rs | 4 +- .../src/bin/short_put_strategy_simulation.rs | 4 +- .../examples_simulation/src/bin/simulator.rs | 9 +- .../src/bin/strategy_simulator.rs | 9 +- .../src/bin/unified_pricing.rs | 18 +- src/chains/generators.rs | 15 +- src/error/simulation.rs | 8 + src/pricing/monte_carlo.rs | 11 +- src/simulation/randomwalk.rs | 135 +++++++++++--- src/simulation/simulator.rs | 151 ++++++++++++--- src/strategies/long_call.rs | 21 ++- src/strategies/long_put.rs | 21 ++- src/strategies/short_call.rs | 21 ++- src/strategies/short_put.rs | 176 +++++++++++++----- tests/unit/chain/random_walk_chain.rs | 15 +- tests/unit/pricing/unified_pricing_test.rs | 33 ++-- .../simulation/model_and_randomwalk_tests.rs | 17 +- 24 files changed, 513 insertions(+), 204 deletions(-) diff --git a/examples/examples_simulation/src/bin/historical_build_chain.rs b/examples/examples_simulation/src/bin/historical_build_chain.rs index f09347be..02bf3ee4 100644 --- a/examples/examples_simulation/src/bin/historical_build_chain.rs +++ b/examples/examples_simulation/src/bin/historical_build_chain.rs @@ -85,9 +85,11 @@ fn main() -> Result<(), Error> { walker, }; - let random_walk = RandomWalk::new("Random Walk".to_string(), &walk_params, |p| { - generator_optionchain(p).expect("generator_optionchain failed") - }); + let random_walk = RandomWalk::new( + "Random Walk".to_string(), + &walk_params, + generator_optionchain, + )?; debug!("Random Walk: {}", random_walk); let path: &Path = "Draws/Simulation/historical_build_chain.png".as_ref(); random_walk.write_png(path)?; diff --git a/examples/examples_simulation/src/bin/long_call_strategy_simulation.rs b/examples/examples_simulation/src/bin/long_call_strategy_simulation.rs index d0332a2c..359b9132 100644 --- a/examples/examples_simulation/src/bin/long_call_strategy_simulation.rs +++ b/examples/examples_simulation/src/bin/long_call_strategy_simulation.rs @@ -159,8 +159,8 @@ fn main() -> Result<(), Error> { "Long Call Simulator".to_string(), n_simulations, &walk_params, - |p| generator_positive(p).expect("generator_positive failed"), - ); + generator_positive, + )?; info!("Running simulations using Simulate trait..."); diff --git a/examples/examples_simulation/src/bin/position_simulator.rs b/examples/examples_simulation/src/bin/position_simulator.rs index 6b1775a6..7df8452b 100644 --- a/examples/examples_simulation/src/bin/position_simulator.rs +++ b/examples/examples_simulation/src/bin/position_simulator.rs @@ -36,9 +36,12 @@ fn main() -> Result<(), Error> { walker, }; - let simulator = Simulator::new("Simulator".to_string(), simulator_size, &walk_params, |p| { - generator_positive(p).expect("generator_positive failed") - }); + let simulator = Simulator::new( + "Simulator".to_string(), + simulator_size, + &walk_params, + generator_positive, + )?; debug!("Simulator: {}", simulator); let option: Options = Options::new( diff --git a/examples/examples_simulation/src/bin/random_walk.rs b/examples/examples_simulation/src/bin/random_walk.rs index 5e199285..bf6d5267 100644 --- a/examples/examples_simulation/src/bin/random_walk.rs +++ b/examples/examples_simulation/src/bin/random_walk.rs @@ -34,9 +34,7 @@ fn main() -> Result<(), Error> { walker, }; - let random_walk = RandomWalk::new("Random Walk".to_string(), &walk_params, |p| { - generator_positive(p).expect("generator_positive failed") - }); + let random_walk = RandomWalk::new("Random Walk".to_string(), &walk_params, generator_positive)?; debug!("Random Walk: {}", random_walk); let path: &std::path::Path = "Draws/Simulation/random_walk.png".as_ref(); random_walk.write_png(path)?; diff --git a/examples/examples_simulation/src/bin/random_walk_build_chain.rs b/examples/examples_simulation/src/bin/random_walk_build_chain.rs index 6763c85f..5260abbe 100644 --- a/examples/examples_simulation/src/bin/random_walk_build_chain.rs +++ b/examples/examples_simulation/src/bin/random_walk_build_chain.rs @@ -67,9 +67,11 @@ fn main() -> Result<(), Error> { walker, }; - let random_walk = RandomWalk::new("Random Walk".to_string(), &walk_params, |p| { - generator_optionchain(p).expect("generator_optionchain failed") - }); + let random_walk = RandomWalk::new( + "Random Walk".to_string(), + &walk_params, + generator_optionchain, + )?; debug!("Random Walk: {}", random_walk); let path: &std::path::Path = "Draws/Simulation/random_walk_build_chain.png".as_ref(); random_walk.write_png(path)?; diff --git a/examples/examples_simulation/src/bin/random_walk_build_series.rs b/examples/examples_simulation/src/bin/random_walk_build_series.rs index cc2de491..6422dbf4 100644 --- a/examples/examples_simulation/src/bin/random_walk_build_series.rs +++ b/examples/examples_simulation/src/bin/random_walk_build_series.rs @@ -80,11 +80,9 @@ fn main() -> Result<(), Error> { }, walker, }; - let random_walk = RandomWalk::new( - "Random Walk".to_string(), - &walk_params, - generator_optionseries, - ); + let random_walk = RandomWalk::new("Random Walk".to_string(), &walk_params, |p| { + Ok::<_, Error>(generator_optionseries(p)) + })?; debug!("Random Walk: {}", random_walk); let path: &std::path::Path = "Draws/Simulation/random_walk_build_series.png".as_ref(); random_walk.write_png(path)?; diff --git a/examples/examples_simulation/src/bin/random_walk_chain.rs b/examples/examples_simulation/src/bin/random_walk_chain.rs index d54c2e88..e0b5bd5f 100644 --- a/examples/examples_simulation/src/bin/random_walk_chain.rs +++ b/examples/examples_simulation/src/bin/random_walk_chain.rs @@ -37,9 +37,11 @@ fn main() -> Result<(), Error> { walker, }; - let random_walk = RandomWalk::new("Random Walk".to_string(), &walk_params, |p| { - generator_optionchain(p).expect("generator_optionchain failed") - }); + let random_walk = RandomWalk::new( + "Random Walk".to_string(), + &walk_params, + generator_optionchain, + )?; debug!("Random Walk: {}", random_walk); let path: &std::path::Path = "Draws/Simulation/random_walk_chain.png".as_ref(); diff --git a/examples/examples_simulation/src/bin/short_put_simulation.rs b/examples/examples_simulation/src/bin/short_put_simulation.rs index b98939e7..9f53f97a 100644 --- a/examples/examples_simulation/src/bin/short_put_simulation.rs +++ b/examples/examples_simulation/src/bin/short_put_simulation.rs @@ -410,8 +410,8 @@ fn main() -> Result<(), Error> { "Short Put Simulator".to_string(), n_simulations, &walk_params, - |p| generator_positive(p).expect("generator_positive failed"), - ); + generator_positive, + )?; // Create progress bar let progress_bar = ProgressBar::new(n_simulations as u64); diff --git a/examples/examples_simulation/src/bin/short_put_strategy_simulation.rs b/examples/examples_simulation/src/bin/short_put_strategy_simulation.rs index c77af7be..e6b96c68 100644 --- a/examples/examples_simulation/src/bin/short_put_strategy_simulation.rs +++ b/examples/examples_simulation/src/bin/short_put_strategy_simulation.rs @@ -152,8 +152,8 @@ fn main() -> Result<(), Error> { "Short Put Simulator".to_string(), n_simulations, &walk_params, - |p| generator_positive(p).expect("generator_positive failed"), - ); + generator_positive, + )?; info!("Running simulations using Simulate trait..."); diff --git a/examples/examples_simulation/src/bin/simulator.rs b/examples/examples_simulation/src/bin/simulator.rs index 0d4fe72f..10bcb968 100644 --- a/examples/examples_simulation/src/bin/simulator.rs +++ b/examples/examples_simulation/src/bin/simulator.rs @@ -37,9 +37,12 @@ fn main() -> Result<(), Error> { walker, }; - let simulator = Simulator::new("Simulator".to_string(), simulator_size, &walk_params, |p| { - generator_positive(p).expect("generator_positive failed") - }); + let simulator = Simulator::new( + "Simulator".to_string(), + simulator_size, + &walk_params, + generator_positive, + )?; debug!("Simulator: {}", simulator); // let last_steps: Vec<&Step> = simulator diff --git a/examples/examples_simulation/src/bin/strategy_simulator.rs b/examples/examples_simulation/src/bin/strategy_simulator.rs index 061cc14b..503e6cfa 100644 --- a/examples/examples_simulation/src/bin/strategy_simulator.rs +++ b/examples/examples_simulation/src/bin/strategy_simulator.rs @@ -53,9 +53,12 @@ fn main() -> Result<(), Error> { walker, }; - let simulator = Simulator::new("Simulator".to_string(), simulator_size, &walk_params, |p| { - generator_positive(p).expect("generator_positive failed") - }); + let simulator = Simulator::new( + "Simulator".to_string(), + simulator_size, + &walk_params, + generator_positive, + )?; debug!("Simulator: {}", simulator); info!("Open Premium: ${:.2}", open_premium); diff --git a/examples/examples_simulation/src/bin/unified_pricing.rs b/examples/examples_simulation/src/bin/unified_pricing.rs index 23cab386..6a3409ec 100644 --- a/examples/examples_simulation/src/bin/unified_pricing.rs +++ b/examples/examples_simulation/src/bin/unified_pricing.rs @@ -20,6 +20,7 @@ use optionstratlib::prelude::*; use positive::pos_or_panic; +use std::convert::Infallible; use std::fmt::Display; use std::ops::AddAssign; @@ -34,7 +35,9 @@ where } // Simple generator for demonstration purposes -fn demo_generator(params: &WalkParams) -> Vec> { +fn demo_generator( + params: &WalkParams, +) -> Result>, Infallible> { let mut out = Vec::with_capacity(params.size); let mut current = params.init_step.clone(); out.push(current.clone()); @@ -44,10 +47,10 @@ fn demo_generator(params: &WalkParams) -> Vec Result<(), Box> { println!("=== Unified Pricing System Example ===\n"); // Create a sample European call option @@ -118,7 +121,7 @@ fn main() { 1000, &gbm_params, demo_generator, - ); + )?; let mc_engine = PricingEngine::MonteCarlo { simulator: gbm_simulator, @@ -152,7 +155,7 @@ fn main() { 1000, &heston_params, demo_generator, - ); + )?; let heston_engine = PricingEngine::MonteCarlo { simulator: heston_simulator, @@ -185,7 +188,7 @@ fn main() { 1000, &jump_params, demo_generator, - ); + )?; let jump_engine = PricingEngine::MonteCarlo { simulator: jump_simulator, @@ -219,7 +222,7 @@ fn main() { 1000, &telegraph_params, demo_generator, - ); + )?; let telegraph_engine = PricingEngine::MonteCarlo { simulator: telegraph_simulator, @@ -230,4 +233,5 @@ fn main() { } println!("=== Example Complete ==="); + Ok(()) } diff --git a/src/chains/generators.rs b/src/chains/generators.rs index e13c5386..601835a0 100644 --- a/src/chains/generators.rs +++ b/src/chains/generators.rs @@ -306,9 +306,12 @@ mod tests { walker, }; - let random_walk = RandomWalk::new("Random Walk".to_string(), &walk_params, |p| { - generator_optionchain(p).unwrap() - }); + let random_walk = RandomWalk::new( + "Random Walk".to_string(), + &walk_params, + generator_optionchain, + ) + .expect("random walk construction"); assert_eq!(random_walk.len(), n_steps); } @@ -341,9 +344,9 @@ mod tests { }, walker, }; - let random_walk = RandomWalk::new("Random Walk".to_string(), &walk_params, |p| { - generator_positive(p).unwrap() - }); + let random_walk = + RandomWalk::new("Random Walk".to_string(), &walk_params, generator_positive) + .expect("random walk construction"); assert_eq!(random_walk.len(), n_steps); } } diff --git a/src/error/simulation.rs b/src/error/simulation.rs index 1e234f29..dd4a976b 100644 --- a/src/error/simulation.rs +++ b/src/error/simulation.rs @@ -149,6 +149,14 @@ impl From for SimulationError { } } +impl From for SimulationError { + fn from(err: crate::error::ChainError) -> Self { + SimulationError::OtherError { + reason: err.to_string(), + } + } +} + /// Type alias for Results that may return a `SimulationError`. /// /// This is a convenience type for functions that return simulation results. diff --git a/src/pricing/monte_carlo.rs b/src/pricing/monte_carlo.rs index 984474a3..6eddb757 100644 --- a/src/pricing/monte_carlo.rs +++ b/src/pricing/monte_carlo.rs @@ -333,9 +333,14 @@ mod tests_price_option_monte_carlo { walker, }; - let simulator = Simulator::new("Test Simulator".to_string(), 100, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new( + "Test Simulator".to_string(), + 100, + &walk_params, + generator_positive, + ) else { + panic!("simulator setup failed"); + }; #[cfg(feature = "static_export")] simulator diff --git a/src/simulation/randomwalk.rs b/src/simulation/randomwalk.rs index 52c11ce9..f3a052f4 100644 --- a/src/simulation/randomwalk.rs +++ b/src/simulation/randomwalk.rs @@ -52,27 +52,35 @@ where { /// Creates a new random walk instance with the given title and steps. /// - /// This constructor takes a title, walk parameters, and a generator function - /// that produces the actual steps of the random walk based on the provided parameters. + /// This constructor takes a title, walk parameters, and a fallible + /// generator function that produces the steps of the random walk + /// from the provided parameters. Errors from the generator are + /// propagated unchanged. /// /// # Parameters /// /// * `title` - A descriptive title for the random walk /// * `params` - Parameters that define the properties of the random walk - /// * `generator` - A function that generates the steps of the random walk + /// * `generator` - A fallible function that generates the steps of + /// the random walk /// /// # Returns /// - /// A new `RandomWalk` instance with the generated steps. + /// `Ok(RandomWalk)` on success, or `Err(E)` when the generator fails. /// - pub fn new(title: String, params: &WalkParams, generator: F) -> Self + /// # Errors + /// + /// Returns the error type produced by the supplied generator. For + /// chain-backed generators (e.g. [`crate::chains::generator_positive`]) + /// this is [`crate::error::ChainError`]. + pub fn new(title: String, params: &WalkParams, generator: F) -> Result where - F: FnOnce(&WalkParams) -> Vec>, + F: FnOnce(&WalkParams) -> Result>, E>, X: Copy + TryInto + AddAssign + Display, Y: TryInto + Display + Clone, { - let steps = generator(params); - Self { title, steps } + let steps = generator(params)?; + Ok(Self { title, steps }) } /// Returns the title of the random walk. @@ -340,6 +348,7 @@ where } #[cfg(test)] +#[allow(irrefutable_let_patterns)] mod tests_random_walk { use super::*; use crate::ExpirationDate; @@ -356,6 +365,7 @@ mod tests_random_walk { use rust_decimal::Decimal; use positive::pos_or_panic; + use std::convert::Infallible; use std::fmt::Display; use std::ops::AddAssign; @@ -411,7 +421,7 @@ mod tests_random_walk { } // Helper function to generate test steps for a random walk - fn generate_test_steps(params: &WalkParams) -> Vec> + fn generate_test_steps(params: &WalkParams) -> Result>, Infallible> where X: Copy + TryInto + AddAssign + Display, Y: TryInto + Display + Clone, @@ -420,7 +430,9 @@ mod tests_random_walk { steps.push(params.init_step.clone()); let test_walker = TestWalker {}; - let values = test_walker.brownian(params).unwrap(); + let values = test_walker + .brownian(params) + .expect("test brownian generator"); let mut current_step = params.init_step.clone(); @@ -430,13 +442,15 @@ mod tests_random_walk { let new_y_value = current_step.y.value(); // Create next step - let next_step = current_step.next(new_y_value.clone()).unwrap(); + let next_step = current_step + .next(new_y_value.clone()) + .expect("test step.next"); steps.push(next_step.clone()); current_step = next_step; } - steps + Ok(steps) } #[test] @@ -453,7 +467,9 @@ mod tests_random_walk { ); let title = "Test Random Walk".to_string(); - let walk = RandomWalk::new(title.clone(), ¶ms, generate_test_steps); + let Ok(walk) = RandomWalk::new(title.clone(), ¶ms, generate_test_steps) else { + unreachable!() + }; assert_eq!(walk.get_title(), title); assert_eq!(walk.len(), 5); @@ -474,7 +490,10 @@ mod tests_random_walk { ); let title = "Empty Walk".to_string(); - let walk = RandomWalk::new(title.clone(), ¶ms, |_| Vec::new()); + let Ok(walk) = RandomWalk::new(title.clone(), ¶ms, |_| Ok::<_, Infallible>(Vec::new())) + else { + unreachable!() + }; assert_eq!(walk.get_title(), title); assert_eq!(walk.len(), 0); @@ -497,7 +516,9 @@ mod tests_random_walk { ); let title = "Initial Title".to_string(); - let mut walk = RandomWalk::new(title, ¶ms, generate_test_steps); + let Ok(mut walk) = RandomWalk::new(title, ¶ms, generate_test_steps) else { + unreachable!() + }; let new_title = "Updated Title".to_string(); walk.set_title(new_title.clone()); @@ -518,7 +539,10 @@ mod tests_random_walk { }, ); - let walk = RandomWalk::new("Test Walk".to_string(), ¶ms, generate_test_steps); + let Ok(walk) = RandomWalk::new("Test Walk".to_string(), ¶ms, generate_test_steps) + else { + unreachable!() + }; let first = walk.first().unwrap(); let last = walk.last().unwrap(); @@ -540,7 +564,10 @@ mod tests_random_walk { }, ); - let walk = RandomWalk::new("Test Walk".to_string(), ¶ms, generate_test_steps); + let Ok(walk) = RandomWalk::new("Test Walk".to_string(), ¶ms, generate_test_steps) + else { + unreachable!() + }; let steps = walk.get_steps(); assert_eq!(steps.len(), 5); @@ -564,7 +591,10 @@ mod tests_random_walk { }, ); - let walk = RandomWalk::new("Test Walk".to_string(), ¶ms, generate_test_steps); + let Ok(walk) = RandomWalk::new("Test Walk".to_string(), ¶ms, generate_test_steps) + else { + unreachable!() + }; let step_0 = walk.get_step(0); let step_3 = walk.get_step(3); @@ -587,7 +617,10 @@ mod tests_random_walk { }, ); - let walk = RandomWalk::new("Test Walk".to_string(), ¶ms, generate_test_steps); + let Ok(walk) = RandomWalk::new("Test Walk".to_string(), ¶ms, generate_test_steps) + else { + unreachable!() + }; // This should panic let _step = walk.get_step(10); @@ -606,7 +639,10 @@ mod tests_random_walk { }, ); - let mut walk = RandomWalk::new("Test Walk".to_string(), ¶ms, generate_test_steps); + let Ok(mut walk) = RandomWalk::new("Test Walk".to_string(), ¶ms, generate_test_steps) + else { + unreachable!() + }; // Get a mutable reference and verify initial state let step_2 = walk.get_step_mut(2); @@ -634,7 +670,10 @@ mod tests_random_walk { }, ); - let walk = RandomWalk::new("Test Walk".to_string(), ¶ms, generate_test_steps); + let Ok(walk) = RandomWalk::new("Test Walk".to_string(), ¶ms, generate_test_steps) + else { + unreachable!() + }; // Test read access via index operator let step_1 = &walk[1]; @@ -657,7 +696,10 @@ mod tests_random_walk { }, ); - let mut walk = RandomWalk::new("Test Walk".to_string(), ¶ms, generate_test_steps); + let Ok(mut walk) = RandomWalk::new("Test Walk".to_string(), ¶ms, generate_test_steps) + else { + unreachable!() + }; // Get initial step via index let initial_index = *walk[2].x.index(); @@ -671,6 +713,33 @@ mod tests_random_walk { assert_ne!(*walk[2].x.index(), initial_index); } + #[test] + fn test_random_walk_new_propagates_generator_error() { + // Regression for #349: ensure the fallible generator's error is + // surfaced unchanged to the caller. + #[derive(Debug, PartialEq)] + struct FakeError(&'static str); + + let params = create_test_params( + 3, + 1.0_f64, + 100.0_f64, + WalkType::Brownian { + dt: Positive::ONE, + drift: Decimal::ZERO, + volatility: pos_or_panic!(0.2), + }, + ); + + let result: Result, FakeError> = + RandomWalk::new("err".to_string(), ¶ms, |_| Err(FakeError("boom"))); + + match result { + Err(FakeError(msg)) => assert_eq!(msg, "boom"), + Ok(_) => panic!("expected generator error to propagate"), + } + } + #[test] fn test_random_walk_display() { let params = create_test_params( @@ -684,7 +753,10 @@ mod tests_random_walk { }, ); - let walk = RandomWalk::new("Display Test".to_string(), ¶ms, generate_test_steps); + let Ok(walk) = RandomWalk::new("Display Test".to_string(), ¶ms, generate_test_steps) + else { + unreachable!() + }; // Test that the display output contains the title let display_output = format!("{walk}"); @@ -704,7 +776,10 @@ mod tests_random_walk { }, ); - let walk = RandomWalk::new("Graph Test".to_string(), ¶ms, generate_test_steps); + let Ok(walk) = RandomWalk::new("Graph Test".to_string(), ¶ms, generate_test_steps) + else { + unreachable!() + }; // Test Graph implementation methods assert_eq!(walk.get_title(), "Graph Test"); @@ -762,21 +837,25 @@ mod tests_random_walk { ); // Custom generator for TestX and TestY - let generator = |params: &WalkParams| { + let generator = |params: &WalkParams| -> Result<_, Infallible> { let mut steps = Vec::new(); steps.push(params.init_step.clone()); let mut current_step = params.init_step.clone(); for i in 1..params.size { - let next_step = current_step.next(TestY((100.0 + i as f64) * 1.1)).unwrap(); + let next_step = current_step + .next(TestY((100.0 + i as f64) * 1.1)) + .expect("test step.next"); steps.push(next_step.clone()); current_step = next_step; } - steps + Ok(steps) }; - let walk = RandomWalk::new("Custom Types Test".to_string(), ¶ms, generator); + let Ok(walk) = RandomWalk::new("Custom Types Test".to_string(), ¶ms, generator) else { + unreachable!() + }; assert_eq!(walk.len(), 3); assert_eq!(*walk[0].y.value(), TestY(100.0)); diff --git a/src/simulation/simulator.rs b/src/simulation/simulator.rs index 7a3a6b26..ad725999 100644 --- a/src/simulation/simulator.rs +++ b/src/simulation/simulator.rs @@ -52,37 +52,51 @@ where X: Copy + TryInto + AddAssign + Display, Y: TryInto + Display + Clone, { - /// Creates a new random walk instance with the given title and steps. + /// Creates a new simulator that builds `size` random walks from the + /// supplied fallible generator. /// - /// This constructor takes a title, walk parameters, and a generator function - /// that produces the actual steps of the random walk based on the provided parameters. + /// The generator is invoked once per random walk; any error short + /// circuits the constructor and no partial `Simulator` is returned. /// /// # Parameters /// - /// * `title` - A descriptive title for the random walk - /// * `params` - Parameters that define the properties of the random walk - /// * `generator` - A function that generates the steps of the random walk + /// * `title` - A descriptive title; individual walks are titled + /// `"{title}_{i}"`. + /// * `size` - Number of random walks to generate. + /// * `params` - Walk parameters shared across all generated walks. + /// * `generator` - A fallible step generator. Cloned per walk; pass a + /// function pointer or a stateless closure for best ergonomics. /// /// # Returns /// - /// A new `RandomWalk` instance with the generated steps. + /// `Ok(Simulator)` on success, or `Err(E)` from the first generator + /// invocation that fails. /// - pub fn new(title: String, size: usize, params: &WalkParams, generator: F) -> Self + /// # Errors + /// + /// Returns the error type produced by the supplied generator. For + /// chain-backed generators (e.g. [`crate::chains::generator_positive`]) + /// this is [`crate::error::ChainError`]. + pub fn new( + title: String, + size: usize, + params: &WalkParams, + generator: F, + ) -> Result where - F: Fn(&WalkParams) -> Vec> + Clone, + F: Fn(&WalkParams) -> Result>, E> + Clone, X: Copy + TryInto + AddAssign + Display, Y: TryInto + Display + Clone, { - let mut random_walks = Vec::new(); + let mut random_walks = Vec::with_capacity(size); for i in 0..size { - let title = format!("{title}_{i}"); - let random_walk = RandomWalk::new(title, params, &generator); - random_walks.push(random_walk); + let walk_title = format!("{title}_{i}"); + random_walks.push(RandomWalk::new(walk_title, params, generator.clone())?); } - Self { + Ok(Self { title, random_walks, - } + }) } /// Returns the title of the random walk. @@ -456,6 +470,7 @@ where } #[cfg(test)] +#[allow(irrefutable_let_patterns)] mod tests { use super::*; use crate::ExpirationDate; @@ -468,6 +483,7 @@ mod tests { use crate::utils::{TimeFrame, time::convert_time_frame}; use positive::pos_or_panic; use rust_decimal_macros::dec; + use std::convert::Infallible; use tracing::{debug, info}; #[cfg(feature = "plotly")] use {std::fs, std::path::Path}; @@ -482,8 +498,10 @@ mod tests { } impl WalkTypeAble for TestWalker {} - fn test_generator(params: &WalkParams) -> Vec> { - vec![params.init_step.clone()] + fn test_generator( + params: &WalkParams, + ) -> Result>, Infallible> { + Ok(vec![params.init_step.clone()]) } // Test Simulator creation @@ -515,12 +533,14 @@ mod tests { walker, }; - let simulator = Simulator::new( + let Ok(simulator) = Simulator::new( "Test Simulator".to_string(), 5, &walk_params, test_generator, - ); + ) else { + unreachable!() + }; assert_eq!(simulator.get_title(), "Test Simulator"); assert_eq!(simulator.len(), 5); @@ -556,12 +576,14 @@ mod tests { walker, }; - let mut simulator = Simulator::new( + let Ok(mut simulator) = Simulator::new( "Original Title".to_string(), 3, &walk_params, test_generator, - ); + ) else { + unreachable!() + }; assert_eq!(simulator.get_title(), "Original Title"); @@ -598,12 +620,14 @@ mod tests { walker, }; - let simulator = Simulator::new( + let Ok(simulator) = Simulator::new( "Test Simulator".to_string(), 3, &walk_params, test_generator, - ); + ) else { + unreachable!() + }; // Test get_steps let steps = simulator.get_random_walks(); @@ -655,12 +679,14 @@ mod tests { walker, }; - let mut simulator = Simulator::new( + let Ok(mut simulator) = Simulator::new( "Test Simulator".to_string(), 3, &walk_params, test_generator, - ); + ) else { + unreachable!() + }; // Test immutable indexing assert_eq!(simulator[0].get_title(), "Test Simulator_0"); @@ -701,7 +727,11 @@ mod tests { walker, }; - let simulator = Simulator::new("Display Test".to_string(), 2, &walk_params, test_generator); + let Ok(simulator) = + Simulator::new("Display Test".to_string(), 2, &walk_params, test_generator) + else { + unreachable!() + }; let display_output = format!("{simulator}"); assert!(display_output.starts_with("Display Test")); @@ -724,6 +754,59 @@ mod tests { assert!(simulator.last().is_none()); } + #[test] + fn test_simulator_new_propagates_generator_error_short_circuits() { + // Regression for #349: ensure the simulator constructor returns + // the first generator error and does not silently build a + // partial simulator. + use std::cell::Cell; + let walker = Box::new(TestWalker); + let initial_price = Positive::HUNDRED; + let init_step = Step { + x: Xstep::new( + Positive::ONE, + TimeFrame::Minute, + ExpirationDate::Days(pos_or_panic!(30.0)), + ), + y: Ystep::new(0, initial_price), + }; + + let walk_params = WalkParams { + size: 1, + init_step, + walk_type: WalkType::GeometricBrownian { + dt: convert_time_frame( + Positive::ONE / pos_or_panic!(30.0), + &TimeFrame::Minute, + &TimeFrame::Day, + ), + drift: dec!(0.0), + volatility: pos_or_panic!(0.2), + }, + walker, + }; + + let calls: Cell = Cell::new(0); + let result: Result, &'static str> = + Simulator::new("Err Sim".to_string(), 5, &walk_params, |p| { + let n = calls.get(); + calls.set(n + 1); + if n == 1 { + Err("boom") + } else { + Ok(vec![p.init_step.clone()]) + } + }); + + match result { + Err(msg) => assert_eq!(msg, "boom"), + Ok(_) => panic!("expected generator error to propagate"), + } + // Generator should have been called twice (success then failure) + // and not five times — short-circuited. + assert_eq!(calls.get(), 2); + } + // Test panic scenarios (these would typically be in separate test functions) #[test] #[should_panic(expected = "index out of bounds")] @@ -754,7 +837,11 @@ mod tests { walker, }; - let simulator = Simulator::new("Panic Test".to_string(), 3, &walk_params, test_generator); + let Ok(simulator) = + Simulator::new("Panic Test".to_string(), 3, &walk_params, test_generator) + else { + unreachable!() + }; // This should panic let _ = simulator[3]; @@ -787,10 +874,12 @@ mod tests { assert_eq!(walk_params.init_step.get_value(), &Positive::HUNDRED); assert_eq!(walk_params.y(), &Positive::HUNDRED); - let simulator = - Simulator::new("Simulator".to_string(), simulator_size, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let simulator = Simulator::new( + "Simulator".to_string(), + simulator_size, + &walk_params, + generator_positive, + )?; debug!("Simulator: {}", simulator); assert_eq!(simulator.get_title(), "Simulator"); assert_eq!(simulator.len(), simulator_size); diff --git a/src/strategies/long_call.rs b/src/strategies/long_call.rs index 10eb3596..122e2808 100644 --- a/src/strategies/long_call.rs +++ b/src/strategies/long_call.rs @@ -816,9 +816,10 @@ mod tests_simulate { pos_or_panic!(115.0), ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test".to_string(), 1, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new("Test".to_string(), 1, &walk_params, generator_positive) + else { + panic!("simulator setup failed"); + }; let results = strategy.simulate(&simulator, ExitPolicy::ProfitPercent(dec!(0.5))); assert!(results.is_ok()); let stats = results.unwrap(); @@ -834,9 +835,10 @@ mod tests_simulate { pos_or_panic!(102.0), ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test".to_string(), 1, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new("Test".to_string(), 1, &walk_params, generator_positive) + else { + panic!("simulator setup failed"); + }; let results = strategy.simulate(&simulator, ExitPolicy::Expiration); assert!(results.is_ok(), "Simulate failed: {:?}", results.err()); let stats = results.unwrap(); @@ -852,9 +854,10 @@ mod tests_simulate { pos_or_panic!(110.0), ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test".to_string(), 3, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new("Test".to_string(), 3, &walk_params, generator_positive) + else { + panic!("simulator setup failed"); + }; let results = strategy.simulate(&simulator, ExitPolicy::Expiration); assert!(results.is_ok(), "Simulate failed: {:?}", results.err()); let stats = results.unwrap(); diff --git a/src/strategies/long_put.rs b/src/strategies/long_put.rs index 7177d1d6..ef9e036b 100644 --- a/src/strategies/long_put.rs +++ b/src/strategies/long_put.rs @@ -825,9 +825,10 @@ mod tests_simulate { pos_or_panic!(85.0), ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test".to_string(), 1, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new("Test".to_string(), 1, &walk_params, generator_positive) + else { + panic!("simulator setup failed"); + }; let results = strategy.simulate(&simulator, ExitPolicy::ProfitPercent(dec!(0.5))); assert!(results.is_ok()); let stats = results.unwrap(); @@ -839,9 +840,10 @@ mod tests_simulate { let strategy = create_test_long_put(); let prices = vec![Positive::HUNDRED, pos_or_panic!(99.0), pos_or_panic!(98.0)]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test".to_string(), 1, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new("Test".to_string(), 1, &walk_params, generator_positive) + else { + panic!("simulator setup failed"); + }; let results = strategy.simulate(&simulator, ExitPolicy::Expiration); assert!(results.is_ok(), "Simulate failed: {:?}", results.err()); let stats = results.unwrap(); @@ -853,9 +855,10 @@ mod tests_simulate { let strategy = create_test_long_put(); let prices = vec![Positive::HUNDRED, pos_or_panic!(95.0), pos_or_panic!(90.0)]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test".to_string(), 3, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new("Test".to_string(), 3, &walk_params, generator_positive) + else { + panic!("simulator setup failed"); + }; let results = strategy.simulate(&simulator, ExitPolicy::Expiration); assert!(results.is_ok(), "Simulate failed: {:?}", results.err()); let stats = results.unwrap(); diff --git a/src/strategies/short_call.rs b/src/strategies/short_call.rs index db1ed4da..c28e6158 100644 --- a/src/strategies/short_call.rs +++ b/src/strategies/short_call.rs @@ -834,9 +834,10 @@ mod tests_simulate { pos_or_panic!(85.0), ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test".to_string(), 1, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new("Test".to_string(), 1, &walk_params, generator_positive) + else { + panic!("simulator setup failed"); + }; let results = strategy.simulate(&simulator, ExitPolicy::ProfitPercent(dec!(0.5))); assert!(results.is_ok()); let stats = results.unwrap(); @@ -848,9 +849,10 @@ mod tests_simulate { let strategy = create_test_short_call(); let prices = vec![Positive::HUNDRED, pos_or_panic!(99.0), pos_or_panic!(98.0)]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test".to_string(), 1, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new("Test".to_string(), 1, &walk_params, generator_positive) + else { + panic!("simulator setup failed"); + }; let results = strategy.simulate(&simulator, ExitPolicy::Expiration); assert!(results.is_ok(), "Simulate failed: {:?}", results.err()); let stats = results.unwrap(); @@ -862,9 +864,10 @@ mod tests_simulate { let strategy = create_test_short_call(); let prices = vec![Positive::HUNDRED, pos_or_panic!(95.0), pos_or_panic!(90.0)]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test".to_string(), 3, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new("Test".to_string(), 3, &walk_params, generator_positive) + else { + panic!("simulator setup failed"); + }; let results = strategy.simulate(&simulator, ExitPolicy::Expiration); assert!(results.is_ok(), "Simulate failed: {:?}", results.err()); let stats = results.unwrap(); diff --git a/src/strategies/short_put.rs b/src/strategies/short_put.rs index 297f24c5..10b2c597 100644 --- a/src/strategies/short_put.rs +++ b/src/strategies/short_put.rs @@ -837,9 +837,14 @@ mod tests_simulate { ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test Simulator".to_string(), 1, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new( + "Test Simulator".to_string(), + 1, + &walk_params, + generator_positive, + ) else { + panic!("simulator setup failed"); + }; let exit_policy = ExitPolicy::ProfitPercent(dec!(0.5)); let results = strategy.simulate(&simulator, exit_policy); @@ -861,9 +866,14 @@ mod tests_simulate { ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test Simulator".to_string(), 1, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new( + "Test Simulator".to_string(), + 1, + &walk_params, + generator_positive, + ) else { + panic!("simulator setup failed"); + }; let exit_policy = ExitPolicy::LossPercent(dec!(1.0)); let results = strategy.simulate(&simulator, exit_policy); @@ -885,9 +895,14 @@ mod tests_simulate { ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test Simulator".to_string(), 1, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new( + "Test Simulator".to_string(), + 1, + &walk_params, + generator_positive, + ) else { + panic!("simulator setup failed"); + }; let exit_policy = ExitPolicy::Expiration; let results = strategy.simulate(&simulator, exit_policy); @@ -909,9 +924,14 @@ mod tests_simulate { ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test Simulator".to_string(), 1, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new( + "Test Simulator".to_string(), + 1, + &walk_params, + generator_positive, + ) else { + panic!("simulator setup failed"); + }; let exit_policy = ExitPolicy::Or(vec![ ExitPolicy::ProfitPercent(dec!(0.5)), @@ -935,9 +955,14 @@ mod tests_simulate { ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test Simulator".to_string(), 5, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new( + "Test Simulator".to_string(), + 5, + &walk_params, + generator_positive, + ) else { + panic!("simulator setup failed"); + }; let exit_policy = ExitPolicy::Expiration; let results = strategy.simulate(&simulator, exit_policy); @@ -963,9 +988,14 @@ mod tests_simulate { ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test Simulator".to_string(), 1, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new( + "Test Simulator".to_string(), + 1, + &walk_params, + generator_positive, + ) else { + panic!("simulator setup failed"); + }; let exit_policy = ExitPolicy::TimeSteps(2); let results = strategy.simulate(&simulator, exit_policy); @@ -982,9 +1012,14 @@ mod tests_simulate { let prices = vec![Positive::HUNDRED, pos_or_panic!(95.0), pos_or_panic!(90.0)]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test Simulator".to_string(), 1, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new( + "Test Simulator".to_string(), + 1, + &walk_params, + generator_positive, + ) else { + panic!("simulator setup failed"); + }; let exit_policy = ExitPolicy::UnderlyingBelow(pos_or_panic!(92.0)); let results = strategy.simulate(&simulator, exit_policy); @@ -1005,9 +1040,14 @@ mod tests_simulate { ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test Simulator".to_string(), 1, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new( + "Test Simulator".to_string(), + 1, + &walk_params, + generator_positive, + ) else { + panic!("simulator setup failed"); + }; let exit_policy = ExitPolicy::And(vec![ ExitPolicy::TimeSteps(2), @@ -1030,9 +1070,14 @@ mod tests_simulate { ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test Simulator".to_string(), 10, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new( + "Test Simulator".to_string(), + 10, + &walk_params, + generator_positive, + ) else { + panic!("simulator setup failed"); + }; let exit_policy = ExitPolicy::Expiration; let results = strategy.simulate(&simulator, exit_policy); @@ -1054,9 +1099,14 @@ mod tests_simulate { ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test Simulator".to_string(), 3, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new( + "Test Simulator".to_string(), + 3, + &walk_params, + generator_positive, + ) else { + panic!("simulator setup failed"); + }; let exit_policy = ExitPolicy::Expiration; let results = strategy.simulate(&simulator, exit_policy); @@ -1083,9 +1133,14 @@ mod tests_simulate { ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test Simulator".to_string(), 1, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new( + "Test Simulator".to_string(), + 1, + &walk_params, + generator_positive, + ) else { + panic!("simulator setup failed"); + }; let exit_policy = ExitPolicy::Expiration; let results = strategy.simulate(&simulator, exit_policy); @@ -1112,9 +1167,14 @@ mod tests_simulate { ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test Simulator".to_string(), 1, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new( + "Test Simulator".to_string(), + 1, + &walk_params, + generator_positive, + ) else { + panic!("simulator setup failed"); + }; let exit_policy = ExitPolicy::ProfitPercent(dec!(0.5)); let results = strategy.simulate(&simulator, exit_policy); @@ -1134,9 +1194,14 @@ mod tests_simulate { let prices = vec![Positive::HUNDRED, pos_or_panic!(85.0), pos_or_panic!(70.0)]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test Simulator".to_string(), 1, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new( + "Test Simulator".to_string(), + 1, + &walk_params, + generator_positive, + ) else { + panic!("simulator setup failed"); + }; let exit_policy = ExitPolicy::LossPercent(dec!(1.0)); let results = strategy.simulate(&simulator, exit_policy); @@ -1160,9 +1225,14 @@ mod tests_simulate { ]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test Simulator".to_string(), 1, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new( + "Test Simulator".to_string(), + 1, + &walk_params, + generator_positive, + ) else { + panic!("simulator setup failed"); + }; let exit_policy = ExitPolicy::Expiration; let results = strategy.simulate(&simulator, exit_policy); @@ -1188,9 +1258,14 @@ mod tests_simulate { let price_count = prices.len(); let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test Simulator".to_string(), 1, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new( + "Test Simulator".to_string(), + 1, + &walk_params, + generator_positive, + ) else { + panic!("simulator setup failed"); + }; let exit_policy = ExitPolicy::TimeSteps(1); let results = strategy.simulate(&simulator, exit_policy); @@ -1209,9 +1284,14 @@ mod tests_simulate { let prices = vec![Positive::HUNDRED, pos_or_panic!(102.0), pos_or_panic!(98.0)]; let walk_params = create_walk_params(prices); - let simulator = Simulator::new("Test Simulator".to_string(), 5, &walk_params, |p| { - generator_positive(p).unwrap() - }); + let Ok(simulator) = Simulator::new( + "Test Simulator".to_string(), + 5, + &walk_params, + generator_positive, + ) else { + panic!("simulator setup failed"); + }; let exit_policy = ExitPolicy::Or(vec![ ExitPolicy::ProfitPercent(dec!(0.5)), diff --git a/tests/unit/chain/random_walk_chain.rs b/tests/unit/chain/random_walk_chain.rs index 63937108..4294378d 100644 --- a/tests/unit/chain/random_walk_chain.rs +++ b/tests/unit/chain/random_walk_chain.rs @@ -41,9 +41,11 @@ fn create_chain_from_step( Ok(new_chain) } -fn generator(walk_params: &WalkParams) -> Vec> { +fn generator( + walk_params: &WalkParams, +) -> Result>, Box> { info!("{}", walk_params); - let mut y_steps = walk_params.walker.geometric_brownian(walk_params).unwrap(); + let mut y_steps = walk_params.walker.geometric_brownian(walk_params)?; let _ = y_steps.remove(0); let mut steps: Vec> = vec![walk_params.init_step.clone()]; let mut previous_x_step = walk_params.init_step.x; @@ -56,7 +58,7 @@ fn generator(walk_params: &WalkParams) -> Vec) -> Vec Result<(), Box> { @@ -102,11 +104,12 @@ fn test_random_walk_chain() -> Result<(), Box> { walker, }; - let random_walk = RandomWalk::new("Random Walk".to_string(), &walk_params, generator); + let random_walk = RandomWalk::new("Random Walk".to_string(), &walk_params, generator)?; info!("Random Walk: {}", random_walk); assert_eq!(random_walk.len(), n_steps); - info!("Last Chain: {}", random_walk.last().unwrap().y.value()); + let last_step = random_walk.last().ok_or("random walk produced no steps")?; + info!("Last Chain: {}", last_step.y.value()); Ok(()) } diff --git a/tests/unit/pricing/unified_pricing_test.rs b/tests/unit/pricing/unified_pricing_test.rs index a8bca8c6..d570c0c7 100644 --- a/tests/unit/pricing/unified_pricing_test.rs +++ b/tests/unit/pricing/unified_pricing_test.rs @@ -13,6 +13,8 @@ use optionstratlib::utils::TimeFrame; use optionstratlib::{ExpirationDate, Options}; use positive::{Positive, pos_or_panic}; use rust_decimal_macros::dec; +use std::convert::Infallible; +use std::error::Error; use std::fmt::Display; use std::ops::AddAssign; @@ -27,7 +29,9 @@ where } /// Simple generator function for testing -fn simple_generator(params: &WalkParams) -> Vec> { +fn simple_generator( + params: &WalkParams, +) -> Result>, Infallible> { let mut out = Vec::with_capacity(params.size); let mut current = params.init_step.clone(); out.push(current.clone()); @@ -37,7 +41,7 @@ fn simple_generator(params: &WalkParams) -> Vec Result<(), Box> { let option = create_test_option(); let init_price = option.underlying_price; let size = 365; @@ -118,7 +122,7 @@ fn test_price_option_monte_carlo() { walk_type: walk, walker: Box::new(TestWalker), }; - let simulator = Simulator::new("MC Test".to_string(), 1000, ¶ms, simple_generator); + let simulator = Simulator::new("MC Test".to_string(), 1000, ¶ms, simple_generator)?; let engine = PricingEngine::MonteCarlo { simulator }; let result = price_option(&option, &engine); @@ -126,10 +130,11 @@ fn test_price_option_monte_carlo() { assert!(result.is_ok(), "Monte Carlo pricing should succeed"); let price = result.unwrap(); assert!(price > Positive::ZERO, "Price should be positive"); + Ok(()) } #[test] -fn test_priceable_trait_monte_carlo() { +fn test_priceable_trait_monte_carlo() -> Result<(), Box> { let option = create_test_option(); let init_price = option.underlying_price; let size = 365; @@ -155,7 +160,7 @@ fn test_priceable_trait_monte_carlo() { walk_type: walk, walker: Box::new(TestWalker), }; - let simulator = Simulator::new("MC Test".to_string(), 1000, ¶ms, simple_generator); + let simulator = Simulator::new("MC Test".to_string(), 1000, ¶ms, simple_generator)?; let engine = PricingEngine::MonteCarlo { simulator }; let result = option.price(&engine); @@ -166,6 +171,7 @@ fn test_priceable_trait_monte_carlo() { ); let price = result.unwrap(); assert!(price > Positive::ZERO, "Price should be positive"); + Ok(()) } #[test] @@ -196,7 +202,7 @@ fn test_short_position_pricing() { } #[test] -fn test_monte_carlo_with_heston() { +fn test_monte_carlo_with_heston() -> Result<(), Box> { let option = create_test_option(); let init_price = option.underlying_price; let size = 365; @@ -226,7 +232,7 @@ fn test_monte_carlo_with_heston() { walk_type: walk, walker: Box::new(TestWalker), }; - let simulator = Simulator::new("Heston Test".to_string(), 500, ¶ms, simple_generator); + let simulator = Simulator::new("Heston Test".to_string(), 500, ¶ms, simple_generator)?; let engine = PricingEngine::MonteCarlo { simulator }; let result = price_option(&option, &engine); @@ -234,10 +240,11 @@ fn test_monte_carlo_with_heston() { assert!(result.is_ok(), "Heston model pricing should succeed"); let price = result.unwrap(); assert!(price > Positive::ZERO, "Price should be positive"); + Ok(()) } #[test] -fn test_monte_carlo_with_jump_diffusion() { +fn test_monte_carlo_with_jump_diffusion() -> Result<(), Box> { let option = create_test_option(); let init_price = option.underlying_price; let size = 365; @@ -271,7 +278,7 @@ fn test_monte_carlo_with_jump_diffusion() { 500, ¶ms, simple_generator, - ); + )?; let engine = PricingEngine::MonteCarlo { simulator }; let result = price_option(&option, &engine); @@ -282,10 +289,11 @@ fn test_monte_carlo_with_jump_diffusion() { ); let price = result.unwrap(); assert!(price > Positive::ZERO, "Price should be positive"); + Ok(()) } #[test] -fn test_monte_carlo_with_telegraph() { +fn test_monte_carlo_with_telegraph() -> Result<(), Box> { let option = create_test_option(); let init_price = option.underlying_price; let size = 365; @@ -315,7 +323,7 @@ fn test_monte_carlo_with_telegraph() { walk_type: walk, walker: Box::new(TestWalker), }; - let simulator = Simulator::new("Telegraph Test".to_string(), 500, ¶ms, simple_generator); + let simulator = Simulator::new("Telegraph Test".to_string(), 500, ¶ms, simple_generator)?; let engine = PricingEngine::MonteCarlo { simulator }; let result = price_option(&option, &engine); @@ -323,6 +331,7 @@ fn test_monte_carlo_with_telegraph() { assert!(result.is_ok(), "Telegraph model pricing should succeed"); let price = result.unwrap(); assert!(price > Positive::ZERO, "Price should be positive"); + Ok(()) } #[test] diff --git a/tests/unit/simulation/model_and_randomwalk_tests.rs b/tests/unit/simulation/model_and_randomwalk_tests.rs index 98b1e250..e5b8510d 100644 --- a/tests/unit/simulation/model_and_randomwalk_tests.rs +++ b/tests/unit/simulation/model_and_randomwalk_tests.rs @@ -7,6 +7,9 @@ use optionstratlib::ExpirationDate;use positive::Positive; use optionstratlib::utils::TimeFrame; use positive::pos_or_panic; use rust_decimal::Decimal; +#![allow(irrefutable_let_patterns)] + +use std::convert::Infallible; use std::error::Error; use std::fmt::Display; use std::ops::AddAssign; @@ -34,7 +37,9 @@ fn make_params(size: usize, start_price: Positive) -> WalkParams) -> Vec> { +fn simple_generator( + params: &WalkParams, +) -> Result>, Infallible> { // Build a tiny deterministic series using Step::next let mut out = Vec::with_capacity(params.size); let mut current = params.init_step.clone(); @@ -45,7 +50,7 @@ fn simple_generator(params: &WalkParams) -> Vec Result<(), Box> { let params = make_params(4, pos_or_panic!(50.0)); // build a simulator with 2 random walks - let mut sim = Simulator::new("SIM".to_string(), 2, ¶ms, simple_generator); + let Ok(mut sim) = Simulator::new("SIM".to_string(), 2, ¶ms, simple_generator) else { + unreachable!() + }; // Accessors across random walks let _rws = sim.get_random_walks(); From 6c012611acef37c66697927dd4c49c44e4dd8f5f Mon Sep 17 00:00:00 2001 From: Joaquin Bejar Date: Fri, 17 Apr 2026 12:40:29 +0200 Subject: [PATCH 3/3] address review: move inner attribute to top of test file --- tests/unit/simulation/model_and_randomwalk_tests.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/unit/simulation/model_and_randomwalk_tests.rs b/tests/unit/simulation/model_and_randomwalk_tests.rs index e5b8510d..486bbc44 100644 --- a/tests/unit/simulation/model_and_randomwalk_tests.rs +++ b/tests/unit/simulation/model_and_randomwalk_tests.rs @@ -1,14 +1,15 @@ -use optionstratlib::simulation::steps::{Step, Xstep, Ystep}; -use optionstratlib::simulation::{WalkParams, WalkType, WalkTypeAble}; +#![allow(irrefutable_let_patterns)] + +use optionstratlib::ExpirationDate; use optionstratlib::simulation::randomwalk::RandomWalk; use optionstratlib::simulation::simulator::Simulator; -use optionstratlib::visualization::Graph; // to exercise graph_data/graph_config -use optionstratlib::ExpirationDate;use positive::Positive; +use optionstratlib::simulation::steps::{Step, Xstep, Ystep}; +use optionstratlib::simulation::{WalkParams, WalkType, WalkTypeAble}; use optionstratlib::utils::TimeFrame; +use optionstratlib::visualization::Graph; // to exercise graph_data/graph_config +use positive::Positive; use positive::pos_or_panic; use rust_decimal::Decimal; -#![allow(irrefutable_let_patterns)] - use std::convert::Infallible; use std::error::Error; use std::fmt::Display;