Skip to main content

optionstratlib_simulation/simulation/
simulator.rs

1use crate::simulation::WalkParams;
2use crate::simulation::randomwalk::RandomWalk;
3use crate::simulation::steps::Step;
4use optionstratlib_core::model::Options;
5use optionstratlib_core::model::Positive;
6use optionstratlib_core::utils::time::ReferenceCarrier;
7use optionstratlib_core::utils::{Len, deterministic_rng};
8use optionstratlib_pricing::error::PricingError;
9use optionstratlib_pricing::pricing::Profit;
10use optionstratlib_pricing::pricing::monte_carlo::price_option_monte_carlo;
11use optionstratlib_pricing::pricing::unified::MonteCarloPricer;
12use rand::RngExt;
13use rayon::prelude::*;
14use rust_decimal::Decimal;
15use std::fmt::Display;
16use std::ops::{AddAssign, Index, IndexMut};
17use tracing::warn;
18
19/// Number of walks from which [`Simulator::new`] builds them on the rayon
20/// pool rather than serially (#860). Measured on an Apple M5 Max (18 cores)
21/// with 30-step geometric Brownian walks: one or two walks build no faster on
22/// the pool than serially, three already build faster. Longer walks only
23/// move the crossover lower.
24pub const PARALLEL_MIN_WALKS: usize = 3;
25
26/// Walks [`Simulator::new`] builds per round on the rayon pool. Each round
27/// draws its walks' seeds in order and returns at its first failing walk, so
28/// a huge `size` never holds more than one round of seeds and results, and a
29/// failing build stops within a round of its first error.
30const PARALLEL_ROUND_WALKS: usize = 1024;
31
32/// Represents a generic simulator for managing and simulating random walks.
33///
34/// # Type Parameters
35/// * `X`: A type that represents the state or value within the random walk. It must adhere to the following bounds:
36///    - `Copy`: Allows for efficient copying of values.
37///    - `TryInto<Positive>`: Ensures values can be converted into a `Positive` type (potentially for validation or numerical operations).
38///    - `AddAssign`: Allows addition and assignment (`+=`) operations.
39///    - `Display`: Enables the formatting of values as strings for user-facing output.
40///
41/// * `Y`: A type that represents the step or transition within the random walk. It must adhere to the following bounds:
42///    - `TryInto<Positive>`: Ensures values can be converted into a `Positive` type.
43///    - `Display`: Enables the formatting of values as strings for user-facing output.
44///    - `Clone`: Allows for creating deep copies of the values.
45///
46/// # Fields
47/// * `title` (`String`): The name or description of the simulator, primarily used for identification or display purposes.
48/// * `random_walks` (`Vec<RandomWalk<X, Y>>`): A collection of `RandomWalk` instances, where each random walk adheres to the defined types `X` and `Y`.
49///
50/// # Usage
51/// This struct is used as a high-level container to manage multiple random walks and perform simulations. Adding specific
52/// functionality such as initializing, running simulations, or generating statistical data depends on additional methods provided
53/// separately.
54///
55/// Note: This struct is generic and requires types provided for both state (`X`) and step/transition (`Y`) that meet the respective
56/// trait bounds.
57#[derive(Debug, Clone)]
58pub struct Simulator<X, Y>
59where
60    X: Copy + TryInto<Positive> + AddAssign + Display,
61    Y: TryInto<Positive> + Display + Clone,
62{
63    title: String,
64    random_walks: Vec<RandomWalk<X, Y>>,
65}
66
67impl<X, Y> Simulator<X, Y>
68where
69    X: Copy + TryInto<Positive> + AddAssign + Display,
70    Y: TryInto<Positive> + Display + Clone,
71{
72    /// Creates a new simulator that builds `size` random walks from the
73    /// supplied fallible generator.
74    ///
75    /// The generator is invoked once per random walk; if any walk fails, no
76    /// partial `Simulator` is returned and the error is the one of the
77    /// lowest-indexed failing walk.
78    ///
79    /// From [`PARALLEL_MIN_WALKS`] walks up, the walks are built on the rayon
80    /// pool and collected in index order (#860). The seed of every walk is
81    /// drawn up front, in order, from the master seed, so a seeded simulator
82    /// is bit-identical to the serial build whatever the thread count. Below
83    /// the threshold the walks are built serially, which is faster there.
84    /// The serial build stops at the first failing walk; the parallel build
85    /// works in rounds of 1024 walks and stops at the end of the round that
86    /// holds the first failing walk, whose error it returns.
87    ///
88    /// # Parameters
89    ///
90    /// * `title` - A descriptive title; individual walks are titled
91    ///   `"{title}_{i}"`.
92    /// * `size` - Number of random walks to generate.
93    /// * `params` - Walk parameters shared across all generated walks.
94    ///   When `params.seed` is `Some(seed)`, walk `i` receives the `i`-th
95    ///   `u64` drawn from
96    ///   [`optionstratlib_core::utils::deterministic_rng`]`(seed)` as its own
97    ///   seed: the walks differ from one another, and the whole simulator is
98    ///   reproducible bit for bit. With `None` every walk draws from the
99    ///   thread RNG.
100    /// * `generator` - A fallible step generator. Cloned per walk; pass a
101    ///   function pointer or a stateless closure for best ergonomics.
102    ///
103    /// # Returns
104    ///
105    /// `Ok(Simulator)` on success, or `Err(E)` from the lowest-indexed walk
106    /// whose generator invocation fails.
107    ///
108    /// # Errors
109    ///
110    /// Returns the error type produced by the supplied generator. For
111    /// chain-backed generators (e.g. [`crate::simulation::generator_positive`])
112    /// this is [`crate::error::SimulationError`].
113    pub fn new<F, E>(
114        title: String,
115        size: usize,
116        params: &WalkParams<X, Y>,
117        generator: F,
118    ) -> Result<Self, E>
119    where
120        F: Fn(&WalkParams<X, Y>) -> Result<Vec<Step<X, Y>>, E> + Clone + Send + Sync,
121        E: Send,
122        X: Copy + TryInto<Positive> + AddAssign + Display + Send + Sync,
123        Y: TryInto<Positive> + Display + Clone + Send + Sync,
124    {
125        // The reservation is only a growth hint, and `E` is the generator's
126        // error type, which cannot carry an allocation failure. A `size` whose
127        // walks cannot be reserved up front used to abort in
128        // `Vec::with_capacity`; it now grows walk by walk, and each walk's own
129        // allocation reports through the generator (#788).
130        let mut random_walks = Vec::new();
131        if let Err(e) = random_walks.try_reserve_exact(size) {
132            warn!(
133                size,
134                error = %e,
135                "Simulator::new: cannot reserve the walks up front; growing on demand"
136            );
137        }
138        // Walk `i` is seeded with the `i`-th `u64` of
139        // `deterministic_rng(seed)`: distinct paths, reproducible run to run,
140        // and independent of how many walks follow. The seeds are drawn in
141        // order before any walk is built, so the parallel build below gives
142        // every walk the seed the serial build gives it.
143        let mut seeds = params.seed.map(deterministic_rng);
144        if size < PARALLEL_MIN_WALKS {
145            let mut walk_params = params.clone();
146            for i in 0..size {
147                if let Some(seeds) = seeds.as_mut() {
148                    walk_params.seed = Some(seeds.random::<u64>());
149                }
150                let walk_title = format!("{title}_{i}");
151                random_walks.push(RandomWalk::new(
152                    walk_title,
153                    &walk_params,
154                    generator.clone(),
155                )?);
156            }
157        } else {
158            // One round at a time: its seeds are drawn in order, its walks
159            // are built on the pool, and they are appended in index order up
160            // to the first error. Memory beyond the walks themselves stays
161            // bounded by the round, and a failing build stops after the round
162            // that holds its first failure.
163            let mut indices = 0..size;
164            loop {
165                let jobs: Vec<(usize, Option<u64>)> = indices
166                    .by_ref()
167                    .take(PARALLEL_ROUND_WALKS)
168                    .map(|i| (i, seeds.as_mut().map(|seeds| seeds.random::<u64>())))
169                    .collect();
170                if jobs.is_empty() {
171                    break;
172                }
173                // The generator's steps may read their days on a rayon worker.
174                let carrier = ReferenceCarrier::capture();
175                let built: Vec<Result<RandomWalk<X, Y>, E>> = jobs
176                    .into_par_iter()
177                    .map_init(
178                        || params.clone(),
179                        |walk_params, (i, seed)| {
180                            if seed.is_some() {
181                                walk_params.seed = seed;
182                            }
183                            carrier.run(|| {
184                                RandomWalk::new(
185                                    format!("{title}_{i}"),
186                                    walk_params,
187                                    generator.clone(),
188                                )
189                            })
190                        },
191                    )
192                    .collect();
193                for walk in built {
194                    random_walks.push(walk?);
195                }
196            }
197        }
198        Ok(Self {
199            title,
200            random_walks,
201        })
202    }
203
204    /// Returns the title of the random walk.
205    ///
206    /// # Returns
207    ///
208    /// A string slice containing the title of the random walk.
209    #[must_use]
210    pub fn get_title(&self) -> &str {
211        &self.title
212    }
213
214    /// Updates the title of the random walk.
215    ///
216    /// # Parameters
217    ///
218    /// * `title` - The new title to set
219    pub fn set_title(&mut self, title: String) {
220        self.title = title;
221    }
222
223    /// Retrieves the steps of the random walks contained within the current object.
224    ///
225    /// This method returns a vector of references to `RandomWalk` instances stored
226    /// in the `random_walks` collection member of the struct. Each `RandomWalk`
227    /// instance represents a step in the random walk process.
228    ///
229    /// # Returns
230    ///
231    /// A `Vec` containing references to `RandomWalk<X, Y>` values, where
232    /// `X` and `Y` are the types used within the random walk structure.
233    ///
234    /// # Note
235    ///
236    /// The returned vector contains borrowed references to the `RandomWalk`
237    /// elements within the struct, and the lifetime of these references
238    /// is tied to the lifetime of the parent object.
239    #[must_use]
240    pub fn get_random_walks(&self) -> Vec<&RandomWalk<X, Y>> {
241        self.random_walks.iter().collect::<Vec<&RandomWalk<X, Y>>>()
242    }
243
244    /// Retrieves a reference to the `RandomWalk` at the specified index.
245    ///
246    /// # Arguments
247    ///
248    /// * `index` - The index of the desired `RandomWalk` within the `random_walks` collection.
249    ///
250    /// # Returns
251    ///
252    /// * `Some(&RandomWalk<X, Y>)` - The walk at `index`.
253    /// * `None` - If `index` is out of bounds.
254    ///
255    /// Named `get_*`, so it follows [`slice::get`] rather than
256    /// [`std::ops::Index`]: an out-of-bounds index is reported, not aborted
257    /// on. Use the `simulator[index]` operator when the index is an invariant
258    /// of the caller and a panic is the wanted contract.
259    #[must_use]
260    pub fn get_random_walk(&self, index: usize) -> Option<&RandomWalk<X, Y>> {
261        self.random_walks.get(index)
262    }
263
264    /// Retrieves a mutable reference to a `RandomWalk` at the specified index.
265    ///
266    /// # Arguments
267    ///
268    /// * `index` - The zero-based index of the `RandomWalk` to access within the `random_walks` collection.
269    ///
270    /// # Returns
271    ///
272    /// * `Some(&mut RandomWalk<X, Y>)` - The walk at `index`.
273    /// * `None` - If `index` is out of bounds.
274    ///
275    /// Mutable counterpart of [`Simulator::get_random_walk`], with the same
276    /// out-of-bounds contract.
277    pub fn get_random_walk_mut(&mut self, index: usize) -> Option<&mut RandomWalk<X, Y>> {
278        self.random_walks.get_mut(index)
279    }
280
281    /// Returns a reference to the first `RandomWalk` element in the `random_walks` collection, if it exists.
282    ///
283    /// # Returns
284    /// - `Some(&RandomWalk<X, Y>)` if the `random_walks` collection is not empty.
285    /// - `None` if the `random_walks` collection is empty.
286    ///
287    #[must_use]
288    pub fn first(&self) -> Option<&RandomWalk<X, Y>> {
289        self.random_walks.first()
290    }
291
292    /// Returns the last random walk in the collection, if it exists.
293    ///
294    /// # Returns
295    /// - `Some(&RandomWalk<X, Y>)`: A reference to the last `RandomWalk` in the collection.
296    /// - `None`: If the collection is empty.
297    ///
298    /// # Note
299    /// The `last` method does not consume the collection; it returns a read-only reference to the last element.
300    #[must_use]
301    pub fn last(&self) -> Option<&RandomWalk<X, Y>> {
302        self.random_walks.last()
303    }
304
305    /// Retrieves a nested vector of references to `Step<X, Y>` objects.
306    ///
307    /// This function iterates over the elements of the current container (`self`)
308    /// assuming it implements `IntoIterator`, and for each element,
309    /// calls its `get_steps` method. The results are then collected into a
310    /// two-dimensional `Vec` structure.
311    ///
312    /// # Returns
313    /// A `Vec` where each inner vector contains references to `Step<X, Y>` objects.
314    ///
315    /// # Type Parameters
316    /// - `X`: The type of the first generic parameter in `Step`.
317    /// - `Y`: The type of the second generic parameter in `Step`.
318    ///
319    #[must_use]
320    pub fn get_steps(&self) -> Vec<Vec<&Step<X, Y>>> {
321        self.into_iter().map(|step| step.get_steps()).collect()
322    }
323
324    /// Returns a `Vec` containing the last `Step` of each item in the iterator.
325    ///
326    /// This method assumes that each item in the iterator is an iterable itself.
327    /// It retrieves the last element of each iterable and collects them into a new `Vec`.
328    ///
329    /// # Returns
330    /// A vector of references to the last `Step<X, Y>` of each non-empty
331    /// random walk. Empty walks are silently skipped, so the returned
332    /// vector may be shorter than `self.len()`.
333    #[must_use]
334    pub fn get_last_steps(&self) -> Vec<&Step<X, Y>> {
335        self.into_iter().filter_map(|step| step.last()).collect()
336    }
337
338    /// Retrieves the last value of each non-empty random walk.
339    ///
340    /// # Returns
341    /// A `Vec` of references to the last `Step<X, Y>` of each non-empty
342    /// random walk. Empty walks are silently skipped, so the returned
343    /// vector may be shorter than `self.len()`.
344    #[must_use]
345    pub fn get_last_values(&self) -> Vec<&Step<X, Y>> {
346        self.into_iter().filter_map(|step| step.last()).collect()
347    }
348
349    /// Retrieves the last set of positive values from the internal state.
350    ///
351    /// This method extracts the positive values from the most recent set of steps retrieved
352    /// by the `last_values` method and returns them as a vector of `Positive` items.
353    ///
354    /// # Returns
355    /// * `Vec<Positive>` - A vector containing the last positive values derived from the steps.
356    ///
357    /// # Notes
358    /// * The `last_values` method is called internally to obtain the most recent set of steps.
359    /// * The positive value for each step is retrieved via the `get_positive_value` method;
360    ///   a step whose value is not a valid `Positive` is left out of the result.
361    #[must_use]
362    pub fn get_last_positive_values(&self) -> Vec<Positive> {
363        let last_values = self.get_last_values();
364        last_values
365            .iter()
366            .filter_map(|step| step.get_positive_value().ok())
367            .collect::<Vec<Positive>>()
368    }
369
370    /// Calculates the price of a financial option using Monte Carlo simulation.
371    ///
372    /// This method computes the price of the provided `option` based on a Monte Carlo
373    /// simulation approach. It retrieves the most recent positive values of the
374    /// underlying asset, which are then used in the simulation to estimate the option's price.
375    ///
376    /// # Arguments
377    /// * `option` - A reference to an `Options` object, representing the financial option
378    ///   to be priced.
379    ///
380    /// # Returns
381    /// * If successful, it returns a `Positive` value wrapped in `Ok`, which represents
382    ///   the computed price of the option.
383    /// * If an error occurs, it returns a `Box<dyn Error>` wrapped in `Err`, indicating
384    ///   the failure during the pricing process.
385    ///
386    /// # Errors
387    /// This function will return an error if:
388    /// * Retrieving the last positive values fails.
389    /// * The Monte Carlo pricing function (`price_option_monte_carlo`) encounters an issue
390    ///   during execution.
391    ///
392    /// # Note
393    /// The implementation assumes that the underlying asset's most recent positive values
394    /// are available and meaningful for Monte Carlo simulation. Ensure that the input data
395    /// and the `option` are valid before invoking this method.
396    pub fn get_mc_option_price(&self, option: &Options) -> Result<Positive, PricingError> {
397        let last_values = self.get_last_positive_values();
398        price_option_monte_carlo(option, &last_values)
399    }
400}
401
402impl MonteCarloPricer for Simulator<Positive, Positive> {
403    /// Delegates to [`Simulator::get_mc_option_price`]: the option is
404    /// priced from the terminal value of every walk the simulator holds.
405    /// Any failure is reported as [`PricingError::SimulationError`], as
406    /// the `MonteCarlo` arm of `price_option` has always done.
407    #[inline]
408    fn price_monte_carlo(&self, option: &Options) -> Result<Positive, PricingError> {
409        self.get_mc_option_price(option)
410            .map_err(|e| PricingError::simulation_error(&e.to_string()))
411    }
412}
413
414impl<X, Y> Len for Simulator<X, Y>
415where
416    X: Copy + TryInto<Positive> + AddAssign + Display,
417    Y: TryInto<Positive> + Display + Clone,
418{
419    /// Returns the number of elements in the `random_walks` collection.
420    ///
421    /// # Returns
422    /// - `usize`: The total count of elements in the `random_walks` collection.
423    ///
424    /// This method is typically used when you need to determine the size
425    /// of the internal `random_walks` data structure.
426    fn len(&self) -> usize {
427        self.random_walks.len()
428    }
429
430    /// Checks if the `random_walks` collection is empty.
431    ///
432    /// # Returns
433    /// * `true` - If the `random_walks` collection contains no elements.
434    /// * `false` - If the `random_walks` collection contains one or more elements.
435    ///
436    fn is_empty(&self) -> bool {
437        self.random_walks.is_empty()
438    }
439}
440
441impl<X, Y> Index<usize> for Simulator<X, Y>
442where
443    X: Copy + TryInto<Positive> + AddAssign + Display,
444    Y: TryInto<Positive> + Display + Clone,
445{
446    /// Defines an alias `Output` for the type `RandomWalk<X, Y>`.
447    ///
448    /// # Type Parameters
449    /// - `X`: Represents the type of the first parameter used in the `RandomWalk`.
450    /// - `Y`: Represents the type of the second parameter used in the `RandomWalk`.
451    ///
452    /// `Output` can be used as a shorthand to refer to a `RandomWalk` instance
453    /// with specific `X` and `Y` types, improving code readability and reducing
454    /// verbosity in the type definitions or method signatures.
455    type Output = RandomWalk<X, Y>;
456
457    /// Retrieves a reference to the element at the specified index in the `random_walks` vector.
458    ///
459    /// # Parameters
460    /// - `index`: The zero-based index of the element to retrieve from the `random_walks` vector.
461    ///
462    /// # Returns
463    /// A reference to the element at the specified `index` in the `random_walks` vector.
464    ///
465    /// # Panics
466    /// This function will panic if the given `index` is out of bounds, i.e., greater than or equal to
467    /// the length of the `random_walks` vector, as indexing a `Vec` does. Use
468    /// [`Simulator::get_random_walk`] for the checked form, which returns `None` instead.
469    ///
470    /// Note: This implementation assumes that `Self` implements the `Index` trait and
471    /// that `random_walks` is a field in the implementing struct.
472    #[allow(clippy::indexing_slicing)]
473    fn index(&self, index: usize) -> &Self::Output {
474        // `Index` / `IndexMut` follow the std contract: an out-of-bounds index
475        // is a caller bug and panics, exactly like `Vec`. The checked
476        // accessor is `get_random_walk`, which returns `None` instead (#341, #788).
477        &self.random_walks[index]
478    }
479}
480
481impl<X, Y> IndexMut<usize> for Simulator<X, Y>
482where
483    X: Copy + TryInto<Positive> + AddAssign + Display,
484    Y: TryInto<Positive> + Display + Clone,
485{
486    /// Retrieves a mutable reference to the walk at `index`.
487    ///
488    /// # Panics
489    ///
490    /// Panics if `index` is out of bounds, as indexing a `Vec` does. Use
491    /// [`Simulator::get_random_walk_mut`] for the checked form, which returns
492    /// `None` instead.
493    #[allow(clippy::indexing_slicing)]
494    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
495        // `Index` / `IndexMut` follow the std contract: an out-of-bounds index
496        // is a caller bug and panics, exactly like `Vec`. The checked
497        // accessor is `get_random_walk_mut`, which returns `None` instead (#341, #788).
498        &mut self.random_walks[index]
499    }
500}
501
502impl<X, Y> Display for Simulator<X, Y>
503where
504    X: Copy + TryInto<Positive> + AddAssign + Display,
505    Y: TryInto<Positive> + Display + Clone,
506{
507    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
508        writeln!(f, "{}", self.title)?;
509        for random_walk in &self.random_walks {
510            writeln!(f, "\t{random_walk}")?;
511        }
512        Ok(())
513    }
514}
515
516impl<X, Y> Profit for Simulator<X, Y>
517where
518    X: AddAssign + Copy + Display + TryInto<Positive>,
519    Y: Clone + Display + TryInto<Positive>,
520{
521    fn calculate_profit_at(&self, _price: &Positive) -> Result<Decimal, PricingError> {
522        Err(PricingError::other(
523            "Profit calculation not implemented for Simulator",
524        ))
525    }
526}
527
528impl<'a, X, Y> IntoIterator for &'a Simulator<X, Y>
529where
530    X: Copy + TryInto<Positive> + AddAssign + Display,
531    Y: TryInto<Positive> + Display + Clone,
532{
533    type Item = &'a RandomWalk<X, Y>;
534    type IntoIter = std::slice::Iter<'a, RandomWalk<X, Y>>;
535
536    fn into_iter(self) -> Self::IntoIter {
537        self.random_walks.iter()
538    }
539}
540
541#[cfg(test)]
542#[allow(irrefutable_let_patterns)]
543mod tests {
544    use super::*;
545    use crate::error::SimulationError;
546    use crate::simulation::generator_positive;
547    use crate::simulation::{
548        WalkParams, WalkType, WalkTypeAble,
549        steps::{Step, Xstep, Ystep},
550    };
551    use optionstratlib_core::model::ExpirationDate;
552    use optionstratlib_core::pos_or_panic;
553    use optionstratlib_core::utils::{TimeFrame, time::convert_time_frame};
554    use optionstratlib_pricing::pricing::OptionPricing;
555    use rust_decimal_macros::dec;
556    use std::convert::Infallible;
557    use tracing::{debug, info};
558
559    /// Seed of the simulated walks, so every run draws the same stream.
560    const SEED: u64 = 685;
561
562    // Helper structs and functions for testing
563    #[derive(Clone)]
564    struct TestWalker;
565
566    impl TestWalker {
567        fn new() -> Self {
568            TestWalker {}
569        }
570    }
571    impl WalkTypeAble<Positive, Positive> for TestWalker {}
572
573    fn test_generator(
574        params: &WalkParams<Positive, Positive>,
575    ) -> Result<Vec<Step<Positive, Positive>>, Infallible> {
576        Ok(vec![params.init_step.clone()])
577    }
578
579    /// The engine prices through `MonteCarloPricer` exactly what the
580    /// simulator produces from the same paths, whether the engine owns the
581    /// simulator or borrows it.
582    #[test]
583    fn test_monte_carlo_pricer_matches_the_simulator_on_the_same_paths() {
584        use optionstratlib_core::model::types::{OptionStyle, OptionType, Side};
585        use optionstratlib_pricing::pricing::{GenericPricingEngine, price_option_with};
586
587        let prices: Vec<Positive> = (0..12)
588            .map(|i| pos_or_panic!(100.0 + f64::from(i) * 1.5))
589            .collect();
590        let params = WalkParams {
591            size: prices.len(),
592            init_step: Step::new(
593                Positive::ONE,
594                TimeFrame::Day,
595                ExpirationDate::Days(pos_or_panic!(30.0)),
596                Positive::HUNDRED,
597            ),
598            walker: Box::new(TestWalker),
599            walk_type: WalkType::Historical {
600                timeframe: TimeFrame::Day,
601                prices,
602                symbol: None,
603            },
604            seed: None,
605        };
606        let simulator =
607            Simulator::new("MC parity".to_string(), 8, &params, generator_positive).unwrap();
608        let option = Options {
609            option_type: OptionType::European,
610            side: Side::Long,
611            underlying_symbol: "TEST".to_string(),
612            strike_price: Positive::HUNDRED,
613            expiration_date: ExpirationDate::Days(pos_or_panic!(30.0)),
614            implied_volatility: pos_or_panic!(0.2),
615            quantity: Positive::ONE,
616            contract_size: Positive::ONE,
617            underlying_price: Positive::HUNDRED,
618            risk_free_rate: dec!(0.05),
619            option_style: OptionStyle::Call,
620            dividend_yield: Positive::ZERO,
621            exotic_params: None,
622        };
623
624        let expected = simulator.get_mc_option_price(&option).unwrap();
625        assert!(expected > Positive::ZERO);
626        assert_eq!(expected, simulator.price_monte_carlo(&option).unwrap());
627
628        let generic = GenericPricingEngine::MonteCarlo {
629            simulator: simulator.clone(),
630        };
631        assert_eq!(price_option_with(&option, &generic).unwrap(), expected);
632
633        let borrowed = GenericPricingEngine::MonteCarlo {
634            simulator: &simulator,
635        };
636        assert_eq!(price_option_with(&option, &borrowed).unwrap(), expected);
637    }
638
639    /// Moved from `pricing::monte_carlo` (#508): the Monte Carlo price
640    /// from the terminal values of a simulated year of paths lands near
641    /// the Black-Scholes price.
642    #[test]
643    fn test_monte_carlo_price_from_simulator_matches_black_scholes()
644    -> Result<(), Box<dyn std::error::Error>> {
645        use optionstratlib_core::assert_pos_relative_eq;
646        use optionstratlib_core::model::utils::create_sample_option;
647        use optionstratlib_core::model::{OptionStyle, Side};
648        use optionstratlib_pricing::pricing::monte_carlo::price_option_monte_carlo;
649
650        let walker = Box::new(TestWalker);
651        let initial_price = pos_or_panic!(1000.0);
652        let days = pos_or_panic!(365.0);
653        let volatility = pos_or_panic!(0.2);
654        let mut option = create_sample_option(
655            OptionStyle::Call,
656            Side::Long,
657            initial_price,
658            Positive::ONE,
659            initial_price,
660            volatility,
661        );
662        option.risk_free_rate = dec!(0.05);
663        option.dividend_yield = pos_or_panic!(0.02);
664        option.expiration_date = ExpirationDate::Days(days);
665
666        let init_step = Step {
667            x: Xstep::new(Positive::ONE, TimeFrame::Day, ExpirationDate::Days(days)),
668            y: Ystep::new(0, initial_price),
669        };
670
671        let dt = convert_time_frame(Positive::ONE, &TimeFrame::Day, &TimeFrame::Year)?;
672        let walk_params = WalkParams {
673            size: 365,
674            init_step,
675            walk_type: WalkType::Custom {
676                dt,
677                drift: dec!(0.02),
678                volatility,
679                vov: pos_or_panic!(0.01),
680                vol_speed: Default::default(),
681                vol_mean: pos_or_panic!(0.2),
682            },
683            walker,
684            seed: Some(SEED),
685        };
686
687        let Ok(simulator) = Simulator::new(
688            "Test Simulator".to_string(),
689            100,
690            &walk_params,
691            generator_positive,
692        ) else {
693            panic!("simulator setup failed");
694        };
695
696        let get_last_positive_values = simulator.get_last_positive_values();
697
698        let result = price_option_monte_carlo(&option, &get_last_positive_values);
699        assert!(result.is_ok());
700
701        let bs = option.calculate_price_black_scholes().unwrap();
702        assert_pos_relative_eq!(
703            result.unwrap(),
704            Positive::new_decimal(bs).unwrap(),
705            pos_or_panic!(10.0)
706        );
707        Ok(())
708    }
709
710    // Test Simulator creation
711    #[test]
712    fn test_simulator_creation() -> Result<(), Box<dyn std::error::Error>> {
713        let walker = Box::new(TestWalker);
714        let initial_price = Positive::HUNDRED;
715        let init_step = Step {
716            x: Xstep::new(
717                Positive::ONE,
718                TimeFrame::Minute,
719                ExpirationDate::Days(pos_or_panic!(30.0)),
720            ),
721            y: Ystep::new(0, initial_price),
722        };
723
724        let walk_params = WalkParams {
725            size: 5,
726            init_step,
727            walk_type: WalkType::GeometricBrownian {
728                dt: convert_time_frame(
729                    Positive::ONE / pos_or_panic!(30.0),
730                    &TimeFrame::Minute,
731                    &TimeFrame::Day,
732                )?,
733                drift: dec!(0.0),
734                volatility: pos_or_panic!(0.2),
735            },
736            walker,
737            seed: Some(SEED),
738        };
739
740        let Ok(simulator) = Simulator::new(
741            "Test Simulator".to_string(),
742            5,
743            &walk_params,
744            test_generator,
745        ) else {
746            unreachable!()
747        };
748
749        assert_eq!(simulator.get_title(), "Test Simulator");
750        assert_eq!(simulator.len(), 5);
751        assert!(!simulator.is_empty());
752        Ok(())
753    }
754
755    // Test title methods
756    #[test]
757    fn test_simulator_title_methods() -> Result<(), Box<dyn std::error::Error>> {
758        let walker = Box::new(TestWalker);
759        let initial_price = Positive::HUNDRED;
760        let init_step = Step {
761            x: Xstep::new(
762                Positive::ONE,
763                TimeFrame::Minute,
764                ExpirationDate::Days(pos_or_panic!(30.0)),
765            ),
766            y: Ystep::new(0, initial_price),
767        };
768
769        let walk_params = WalkParams {
770            size: 3,
771            init_step,
772            walk_type: WalkType::GeometricBrownian {
773                dt: convert_time_frame(
774                    Positive::ONE / pos_or_panic!(30.0),
775                    &TimeFrame::Minute,
776                    &TimeFrame::Day,
777                )?,
778                drift: dec!(0.0),
779                volatility: pos_or_panic!(0.2),
780            },
781            walker,
782            seed: Some(SEED),
783        };
784
785        let Ok(mut simulator) = Simulator::new(
786            "Original Title".to_string(),
787            3,
788            &walk_params,
789            test_generator,
790        ) else {
791            unreachable!()
792        };
793
794        assert_eq!(simulator.get_title(), "Original Title");
795
796        simulator.set_title("New Title".to_string());
797        assert_eq!(simulator.get_title(), "New Title");
798        Ok(())
799    }
800
801    // Test step access methods
802    #[test]
803    fn test_simulator_step_access() -> Result<(), Box<dyn std::error::Error>> {
804        let walker = Box::new(TestWalker);
805        let initial_price = Positive::HUNDRED;
806        let init_step = Step {
807            x: Xstep::new(
808                Positive::ONE,
809                TimeFrame::Minute,
810                ExpirationDate::Days(pos_or_panic!(30.0)),
811            ),
812            y: Ystep::new(0, initial_price),
813        };
814
815        let walk_params = WalkParams {
816            size: 3,
817            init_step,
818            walk_type: WalkType::GeometricBrownian {
819                dt: convert_time_frame(
820                    Positive::ONE / pos_or_panic!(30.0),
821                    &TimeFrame::Minute,
822                    &TimeFrame::Day,
823                )?,
824                drift: dec!(0.0),
825                volatility: pos_or_panic!(0.2),
826            },
827            walker,
828            seed: Some(SEED),
829        };
830
831        let Ok(simulator) = Simulator::new(
832            "Test Simulator".to_string(),
833            3,
834            &walk_params,
835            test_generator,
836        ) else {
837            unreachable!()
838        };
839
840        // Test get_steps
841        let steps = simulator.get_random_walks();
842        assert_eq!(steps.len(), 3);
843
844        // Test get_step
845        let step = simulator.get_random_walk(1).unwrap();
846        assert_eq!(step.get_title(), "Test Simulator_1");
847        // Out of bounds is reported, not aborted on; the panicking contract
848        // lives on the `Index` operator, covered by
849        // `test_simulator_index_out_of_bounds`.
850        assert!(simulator.get_random_walk(99).is_none());
851
852        // Test first and last
853        assert!(simulator.first().is_some());
854        assert!(simulator.last().is_some());
855        assert_eq!(
856            simulator.first().expect("should be Ok").get_title(),
857            "Test Simulator_0"
858        );
859        assert_eq!(
860            simulator.last().expect("should be Ok").get_title(),
861            "Test Simulator_2"
862        );
863        Ok(())
864    }
865
866    // Test Index and IndexMut traits
867    #[test]
868    fn test_simulator_indexing() -> Result<(), Box<dyn std::error::Error>> {
869        let walker = Box::new(TestWalker);
870        let initial_price = Positive::HUNDRED;
871        let init_step = Step {
872            x: Xstep::new(
873                Positive::ONE,
874                TimeFrame::Minute,
875                ExpirationDate::Days(pos_or_panic!(30.0)),
876            ),
877            y: Ystep::new(0, initial_price),
878        };
879
880        let walk_params = WalkParams {
881            size: 3,
882            init_step,
883            walk_type: WalkType::GeometricBrownian {
884                dt: convert_time_frame(
885                    Positive::ONE / pos_or_panic!(30.0),
886                    &TimeFrame::Minute,
887                    &TimeFrame::Day,
888                )?,
889                drift: dec!(0.0),
890                volatility: pos_or_panic!(0.2),
891            },
892            walker,
893            seed: Some(SEED),
894        };
895
896        let Ok(mut simulator) = Simulator::new(
897            "Test Simulator".to_string(),
898            3,
899            &walk_params,
900            test_generator,
901        ) else {
902            unreachable!()
903        };
904
905        // Test immutable indexing
906        assert_eq!(simulator[0].get_title(), "Test Simulator_0");
907        assert_eq!(simulator[1].get_title(), "Test Simulator_1");
908        assert_eq!(simulator[2].get_title(), "Test Simulator_2");
909
910        // Test mutable indexing
911        simulator[1].set_title("Modified Title".to_string());
912        assert_eq!(simulator[1].get_title(), "Modified Title");
913        Ok(())
914    }
915
916    // Test display formatting
917    #[test]
918    fn test_simulator_display() -> Result<(), Box<dyn std::error::Error>> {
919        let walker = Box::new(TestWalker);
920        let initial_price = Positive::HUNDRED;
921        let init_step = Step {
922            x: Xstep::new(
923                Positive::ONE,
924                TimeFrame::Minute,
925                ExpirationDate::Days(pos_or_panic!(30.0)),
926            ),
927            y: Ystep::new(0, initial_price),
928        };
929
930        let walk_params = WalkParams {
931            size: 2,
932            init_step,
933            walk_type: WalkType::GeometricBrownian {
934                dt: convert_time_frame(
935                    Positive::ONE / pos_or_panic!(30.0),
936                    &TimeFrame::Minute,
937                    &TimeFrame::Day,
938                )?,
939                drift: dec!(0.0),
940                volatility: pos_or_panic!(0.2),
941            },
942            walker,
943            seed: Some(SEED),
944        };
945
946        let Ok(simulator) =
947            Simulator::new("Display Test".to_string(), 2, &walk_params, test_generator)
948        else {
949            unreachable!()
950        };
951
952        let display_output = format!("{simulator}");
953        assert!(display_output.starts_with("Display Test"));
954        assert!(display_output.contains("Display Test_0"));
955        assert!(display_output.contains("Display Test_1"));
956        Ok(())
957    }
958
959    // Test simulator with empty collection
960    #[test]
961    fn test_simulator_empty() {
962        let simulator: Simulator<Positive, Positive> = Simulator {
963            title: "Empty Simulator".to_string(),
964            random_walks: Vec::new(),
965        };
966
967        assert_eq!(simulator.get_title(), "Empty Simulator");
968        assert_eq!(simulator.len(), 0);
969        assert!(simulator.is_empty());
970        assert!(simulator.first().is_none());
971        assert!(simulator.last().is_none());
972    }
973
974    #[test]
975    fn test_simulator_new_propagates_generator_error_short_circuits()
976    -> Result<(), Box<dyn std::error::Error>> {
977        // Regression for #349: ensure the simulator constructor returns
978        // the first generator error and does not silently build a
979        // partial simulator.
980        use std::sync::atomic::{AtomicU32, Ordering};
981        let walker = Box::new(TestWalker);
982        let initial_price = Positive::HUNDRED;
983        let init_step = Step {
984            x: Xstep::new(
985                Positive::ONE,
986                TimeFrame::Minute,
987                ExpirationDate::Days(pos_or_panic!(30.0)),
988            ),
989            y: Ystep::new(0, initial_price),
990        };
991
992        let walk_params = WalkParams {
993            size: 1,
994            init_step,
995            walk_type: WalkType::GeometricBrownian {
996                dt: convert_time_frame(
997                    Positive::ONE / pos_or_panic!(30.0),
998                    &TimeFrame::Minute,
999                    &TimeFrame::Day,
1000                )?,
1001                drift: dec!(0.0),
1002                volatility: pos_or_panic!(0.2),
1003            },
1004            walker,
1005            seed: Some(SEED),
1006        };
1007
1008        // The generator is shared across the rayon pool from
1009        // `PARALLEL_MIN_WALKS` walks up, so it must be `Sync`: the call count
1010        // is an atomic. One walk short of the threshold the walks are built
1011        // serially, which stops at the first error.
1012        let calls = AtomicU32::new(0);
1013        let size = PARALLEL_MIN_WALKS - 1;
1014        let result: Result<Simulator<Positive, Positive>, &'static str> =
1015            Simulator::new("Err Sim".to_string(), size, &walk_params, |p| {
1016                let n = calls.fetch_add(1, Ordering::SeqCst);
1017                if n == 0 {
1018                    Err("boom")
1019                } else {
1020                    Ok(vec![p.init_step.clone()])
1021                }
1022            });
1023
1024        match result {
1025            Err(msg) => assert_eq!(msg, "boom"),
1026            Ok(_) => panic!("expected generator error to propagate"),
1027        }
1028        // The first walk failed, so no later walk was built.
1029        assert_eq!(calls.load(Ordering::SeqCst), 1);
1030        Ok(())
1031    }
1032
1033    // Test panic scenarios (these would typically be in separate test functions)
1034    #[test]
1035    #[should_panic(expected = "index out of bounds")]
1036    fn test_simulator_index_out_of_bounds() {
1037        let walker = Box::new(TestWalker);
1038        let initial_price = Positive::HUNDRED;
1039        let init_step = Step {
1040            x: Xstep::new(
1041                Positive::ONE,
1042                TimeFrame::Minute,
1043                ExpirationDate::Days(pos_or_panic!(30.0)),
1044            ),
1045            y: Ystep::new(0, initial_price),
1046        };
1047
1048        let walk_params = WalkParams {
1049            size: 3,
1050            init_step,
1051            walk_type: WalkType::GeometricBrownian {
1052                dt: match convert_time_frame(
1053                    Positive::ONE / pos_or_panic!(30.0),
1054                    &TimeFrame::Minute,
1055                    &TimeFrame::Day,
1056                ) {
1057                    Ok(value) => value,
1058                    Err(error) => panic!("convert_time_frame failed: {error}"),
1059                },
1060                drift: dec!(0.0),
1061                volatility: pos_or_panic!(0.2),
1062            },
1063            walker,
1064            seed: Some(SEED),
1065        };
1066
1067        let Ok(simulator) =
1068            Simulator::new("Panic Test".to_string(), 3, &walk_params, test_generator)
1069        else {
1070            unreachable!()
1071        };
1072
1073        // This should panic
1074        let _ = simulator[3];
1075    }
1076
1077    #[test]
1078    fn test_full_simulation() -> Result<(), SimulationError> {
1079        let simulator_size: usize = 5;
1080        let n_steps = 10;
1081        let initial_price = Positive::HUNDRED;
1082        let std_dev = pos_or_panic!(20.0);
1083        let walker = Box::new(TestWalker::new());
1084        let days = Positive::TWO;
1085
1086        let walk_params = WalkParams {
1087            size: n_steps,
1088            init_step: Step {
1089                x: Xstep::new(Positive::ONE, TimeFrame::Hour, ExpirationDate::Days(days)),
1090                y: Ystep::new(0, initial_price),
1091            },
1092            walk_type: WalkType::GeometricBrownian {
1093                dt: convert_time_frame(Positive::ONE / days, &TimeFrame::Hour, &TimeFrame::Day)?,
1094                drift: dec!(0.0),
1095                volatility: std_dev,
1096            },
1097            walker,
1098            seed: Some(SEED),
1099        };
1100
1101        assert_eq!(walk_params.size, n_steps);
1102        assert_eq!(walk_params.init_step.get_value(), &Positive::HUNDRED);
1103        assert_eq!(walk_params.y(), &Positive::HUNDRED);
1104
1105        let simulator = Simulator::new(
1106            "Simulator".to_string(),
1107            simulator_size,
1108            &walk_params,
1109            generator_positive,
1110        )?;
1111        debug!("Simulator: {}", simulator);
1112        assert_eq!(simulator.get_title(), "Simulator");
1113        assert_eq!(simulator.len(), simulator_size);
1114
1115        let random_walk = simulator[0].clone();
1116        assert_eq!(random_walk.get_title(), "Simulator_0");
1117        assert_eq!(random_walk.len(), n_steps);
1118
1119        let step = random_walk[0].clone();
1120        assert_eq!(*step.get_index(), Positive::ONE);
1121        let step_string = format!("{step}");
1122        assert_eq!(step.to_string(), step_string);
1123
1124        let y_step = step.get_y_step();
1125        assert_eq!(*y_step.index(), 0);
1126        assert_eq!(*y_step.value(), Positive::HUNDRED);
1127
1128        let x_step = step.get_x_step();
1129        assert_eq!(*x_step.index(), 0);
1130        assert_eq!(*x_step.step_size_in_time(), Positive::ONE);
1131        assert_eq!(x_step.time_unit(), &TimeFrame::Hour);
1132        assert_eq!(x_step.days_left()?, Positive::TWO);
1133
1134        let next_step = step.next(pos_or_panic!(200.0)).expect("should be Ok");
1135        assert_eq!(next_step.get_value(), &pos_or_panic!(200.0));
1136        let next_step_string = format!("{next_step}");
1137        assert_eq!(next_step.to_string(), next_step_string);
1138
1139        let previous_step = step.previous(pos_or_panic!(50.0))?;
1140        assert_eq!(previous_step.get_value(), &pos_or_panic!(50.0));
1141        let previous_step_string = format!("{previous_step}");
1142        assert_eq!(previous_step.to_string(), previous_step_string);
1143
1144        let x_step = step.get_x_step();
1145        let next_x_step = x_step.next().expect("should be Ok");
1146        assert_eq!(*next_x_step.index(), 1);
1147        assert_eq!(*next_x_step.step_size_in_time(), Positive::ONE);
1148        let next_x_step_string = format!("{next_x_step}");
1149        assert_eq!(next_x_step.to_string(), next_x_step_string);
1150
1151        let y_step = step.get_y_step();
1152        assert_eq!(*y_step.index(), 0);
1153        assert_eq!(*y_step.value(), Positive::HUNDRED);
1154        assert_eq!(y_step.positive().unwrap(), Positive::HUNDRED);
1155
1156        let last_steps: Vec<&Step<Positive, Positive>> = simulator
1157            .into_iter()
1158            .map(|step| step.last().expect("should be Ok"))
1159            .collect();
1160        info!("Last Steps: {:?}", last_steps);
1161        assert_eq!(last_steps.len(), simulator_size);
1162
1163        let last_values: Vec<&Positive> = simulator
1164            .into_iter()
1165            .map(|step| step.last().expect("should be Ok").get_value())
1166            .collect();
1167        info!("Last Values: {:?}", last_values);
1168        assert_eq!(last_values.len(), simulator_size);
1169
1170        Ok(())
1171    }
1172}
1173
1174/// `Simulator::new` reserved `Vec::with_capacity(size)` up front, which hit
1175/// `capacity overflow` for a `size` whose walks cannot fit in memory before
1176/// the generator ever ran (#788).
1177#[cfg(test)]
1178mod tests_new_panic_freedom {
1179    use super::*;
1180    use crate::simulation::{WalkType, WalkTypeAble};
1181    use optionstratlib_core::model::ExpirationDate;
1182    use optionstratlib_core::utils::TimeFrame;
1183    use rust_decimal::Decimal;
1184
1185    #[derive(Debug, Clone)]
1186    struct BareWalker;
1187
1188    impl WalkTypeAble<Positive, Positive> for BareWalker {}
1189
1190    #[test]
1191    fn test_new_unreservable_size_reaches_the_generator() {
1192        let params = WalkParams {
1193            size: 3,
1194            init_step: Step::new(
1195                Positive::ONE,
1196                TimeFrame::Day,
1197                ExpirationDate::Days(Positive::HUNDRED),
1198                Positive::HUNDRED,
1199            ),
1200            walk_type: WalkType::Brownian {
1201                dt: Positive::ONE,
1202                drift: Decimal::ZERO,
1203                volatility: Positive::ONE,
1204            },
1205            walker: Box::new(BareWalker),
1206            seed: Some(788),
1207        };
1208        let result: Result<Simulator<Positive, Positive>, &'static str> =
1209            Simulator::new("Huge".to_string(), usize::MAX, &params, |_| Err("boom"));
1210        assert!(matches!(result, Err("boom")));
1211    }
1212}