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