Skip to main content

optionstratlib_simulation/simulation/
params.rs

1use crate::error::SimulationError;
2use crate::simulation::steps::{Step, Ystep};
3use crate::simulation::{WalkType, WalkTypeAble};
4use optionstratlib_core::model::Positive;
5use std::convert::TryInto;
6use std::fmt::{Display, Formatter};
7use std::ops::AddAssign;
8
9/// Parameters for stochastic process simulations (random walks).
10///
11/// This struct defines the configuration for generating random walks or price path simulations,
12/// particularly useful in financial modeling, option pricing, and risk analysis contexts.
13///
14/// The generic parameters allow flexibility in the types of steps and values used in the walk,
15/// with appropriate trait bounds to ensure mathematical operations can be performed correctly.
16///
17/// # Type Parameters
18///
19/// * `X` - The type for the x-axis steps (typically time), must support addition and conversion to positive values
20/// * `Y` - The type for the y-axis values (typically price or rate), must support addition, conversion to positive values,
21///   and implement the `Walktypable` trait for traversal operations
22///
23/// # Fields
24///
25/// * `size` - Number of steps or data points to generate in the simulation
26/// * `init_step` - Initial step values (starting point) for the random walk
27/// * `walk_type` - The specific stochastic process algorithm to use for the simulation
28/// * `walker` - Implementation of the walk algorithm that satisfies the `WalkTypeAble` trait
29/// * `seed` - Optional seed for the walk's random stream; `None` draws from the thread RNG
30///
31/// # Seeding
32///
33/// With `seed: None` every stochastic walk draws its normal samples from the
34/// thread-local RNG ([`rand::rng()`]), so two runs differ. With
35/// `seed: Some(s)` the built-in walk kernels draw from
36/// [`optionstratlib_core::utils::deterministic_rng`]`(s)` instead, one
37/// generator per generated path, so the same parameters and seed produce the
38/// same path bit for bit on every run with a given version of `rand` and
39/// `rand_distr`. That is the scope of the promise: `StdRng` and
40/// `StandardNormal` may change their streams across releases, and a
41/// dependency bump that does so is a reviewed change of the pinned fixtures,
42/// not a silent one. [`WalkType::Historical`] replays its
43/// prices and ignores the seed. A walker that overrides a walk method owns
44/// its own randomness and decides whether to honour `seed`.
45///
46/// [`crate::simulation::simulator::Simulator::new`] derives a distinct seed
47/// for each of its walks from this one, so a seeded simulator is reproducible
48/// without every walk repeating the same path.
49///
50/// # Usage
51///
52/// This struct is typically instantiated at the beginning of a simulation process to configure
53/// how random walks will be generated. It provides the foundation for various financial simulations
54/// including price path forecasting, Monte Carlo simulations for options pricing, and risk analysis models.
55///
56#[derive(Debug, Clone)]
57pub struct WalkParams<X, Y>
58where
59    X: Copy + TryInto<Positive> + AddAssign + Display + Sized,
60    Y: TryInto<Positive> + Display + Sized + Clone,
61{
62    /// Number of steps or data points to generate in the simulation
63    /// Determines the resolution and length of the resulting random walk
64    pub size: usize,
65
66    /// Initial step values (starting point) for the random walk
67    /// Typically represents initial time and price/rate values
68    pub init_step: Step<X, Y>,
69
70    /// The specific stochastic process to use for generating the random walk
71    /// Determines the mathematical properties and behavior of the simulated path
72    pub walk_type: WalkType,
73
74    /// Implementation of the walk algorithm that satisfies the WalkTypeAble trait
75    /// Provides the concrete logic for generating steps according to the selected walk_type
76    pub walker: Box<dyn WalkTypeAble<X, Y>>,
77
78    /// Seed of the walk's random stream. `None` draws from the thread RNG
79    /// (non-reproducible); `Some(seed)` makes every stochastic built-in walk
80    /// reproducible bit for bit for a given `rand` / `rand_distr` version.
81    /// See the struct-level "Seeding" section.
82    pub seed: Option<u64>,
83}
84
85/// Access methods for the initial y-axis step value.
86///
87/// These methods provide different ways to access and utilize the initial
88/// value of the y-axis step, which is typically associated with the starting
89/// price or other relevant metric in a financial simulation.  The y value
90/// is wrapped in a `Ystep<Y>` which keeps track of the step index and
91/// ensures the value can be converted to a positive number.
92impl<X, Y> WalkParams<X, Y>
93where
94    X: Copy + TryInto<Positive> + AddAssign + Display + Sized,
95    Y: TryInto<Positive> + Display + Sized + Clone,
96{
97    /// Returns an immutable reference to the initial y-axis value.
98    ///
99    /// This provides direct access to the underlying value without copying.
100    ///
101    /// # Returns
102    ///
103    /// An immutable reference to the initial y-axis value of type `&Y`.
104    pub fn y(&self) -> &Y {
105        self.init_step.y.value()
106    }
107
108    /// Returns an immutable reference to the `Ystep<Y>` object.
109    ///
110    /// This allows access to the full `Ystep` functionality, including
111    /// the step index.
112    ///
113    /// # Returns
114    ///
115    /// An immutable reference to the initial `Ystep<Y>` object.
116    pub fn ystep_ref(&self) -> &Ystep<Y> {
117        &self.init_step.y
118    }
119
120    /// Returns a copy of the `Ystep<Y>` object.
121    ///
122    /// This creates a new independent copy of the `Ystep`.
123    ///
124    /// # Returns
125    ///
126    /// A copy of the initial `Ystep<Y>` object.
127    pub fn ystep(&self) -> Ystep<Y> {
128        self.init_step.y.clone()
129    }
130
131    /// Returns the initial y-axis step value as a `Positive` number.
132    ///
133    /// This ensures the value is positive, which is often a requirement
134    /// in financial calculations.
135    ///
136    /// # Returns
137    ///
138    /// The initial y-axis value as a `Positive` number.
139    ///
140    /// # Errors
141    ///
142    /// Propagates any [`SimulationError`] returned by
143    /// [`Ystep::positive`] (typically
144    /// [`SimulationError::PositiveError`] when the current `y` value
145    /// breaches the `Positive` invariant).
146    pub fn ystep_as_positive(&self) -> Result<Positive, SimulationError> {
147        self.ystep_ref().positive()
148    }
149}
150
151impl<X, Y> Display for WalkParams<X, Y>
152where
153    X: Copy + TryInto<Positive> + AddAssign + Display,
154    Y: TryInto<Positive> + Display + Clone,
155{
156    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
157        write!(
158            f,
159            "WalkParams {{ size: {}, init_step: {}, walk_type: {}, seed: {:?} }}",
160            self.size, self.init_step, self.walk_type, self.seed
161        )
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use crate::simulation::steps::{Xstep, Ystep};
169    use optionstratlib_core::model::ExpirationDate;
170    use optionstratlib_core::pos_or_panic;
171    use optionstratlib_core::utils::time::TimeFrame;
172    use rust_decimal::Decimal;
173    use rust_decimal_macros::dec;
174    use std::fmt::Display;
175    use std::ops::AddAssign;
176
177    #[derive(Clone)]
178    struct MockWalker;
179
180    impl<X, Y> WalkTypeAble<X, Y> for MockWalker
181    where
182        X: Copy + TryInto<Positive> + AddAssign + Display,
183        Y: Copy + TryInto<Positive> + Display,
184    {
185    }
186
187    #[test]
188    fn test_walk_params_creation_with_positive() {
189        let init_x = Xstep::new(
190            Positive::ONE,
191            TimeFrame::Day,
192            ExpirationDate::Days(pos_or_panic!(30.0)),
193        );
194        let init_y = Ystep::new(0, Positive::HUNDRED);
195        let init_step = Step {
196            x: init_x,
197            y: init_y,
198        };
199
200        let walk_params = WalkParams {
201            size: 100,
202            init_step,
203            walk_type: WalkType::Brownian {
204                dt: Positive::ONE,
205                drift: Decimal::ZERO,
206                volatility: pos_or_panic!(0.2),
207            },
208            walker: Box::new(MockWalker),
209            seed: None,
210        };
211
212        assert_eq!(walk_params.size, 100);
213        assert_eq!(
214            walk_params.init_step.x.step_size_in_time().value(),
215            Positive::ONE.value()
216        );
217        assert_eq!(
218            walk_params.init_step.y.value().value(),
219            Positive::HUNDRED.value()
220        );
221    }
222
223    #[test]
224    fn test_walk_params_clone_with_positive() {
225        let init_x = Xstep::new(
226            Positive::ONE,
227            TimeFrame::Day,
228            ExpirationDate::Days(pos_or_panic!(30.0)),
229        );
230        let init_y = Ystep::new(0, Positive::HUNDRED);
231        let init_step = Step {
232            x: init_x,
233            y: init_y,
234        };
235        let walk_params = WalkParams {
236            size: 100,
237            init_step,
238            walk_type: WalkType::Brownian {
239                dt: Positive::ONE,
240                drift: Decimal::ZERO,
241                volatility: pos_or_panic!(0.2),
242            },
243            walker: Box::new(MockWalker),
244            seed: None,
245        };
246        let cloned_params = &walk_params;
247
248        assert_eq!(cloned_params.size, walk_params.size);
249        assert_eq!(
250            cloned_params.init_step.x.step_size_in_time().value(),
251            walk_params.init_step.x.step_size_in_time().value()
252        );
253        assert_eq!(
254            cloned_params.init_step.y.value().value(),
255            walk_params.init_step.y.value().value()
256        );
257    }
258
259    #[test]
260    fn test_walk_params_display_with_positive() {
261        let init_x = Xstep::new(
262            pos_or_panic!(1.5),
263            TimeFrame::Day,
264            ExpirationDate::Days(pos_or_panic!(30.0)),
265        );
266        let init_y = Ystep::new(0, pos_or_panic!(200.0));
267        let init_step = Step {
268            x: init_x,
269            y: init_y,
270        };
271
272        let walk_params = WalkParams {
273            size: 50,
274            init_step: init_step.clone(),
275            walk_type: WalkType::GeometricBrownian {
276                dt: pos_or_panic!(1.0 / 252.0),
277                drift: dec!(0.0),
278                volatility: pos_or_panic!(0.2),
279            },
280            walker: Box::new(MockWalker),
281            // Unseeded on purpose: the test pins how `Display` renders `seed: None`.
282            seed: None,
283        };
284
285        let display_string = format!("{walk_params}");
286
287        // Check that the display string contains all the expected parts
288        assert!(display_string.contains("size: 50"));
289        assert!(display_string.contains(&format!("{init_step}")));
290        assert!(display_string.contains("walk_type: GeometricBrownian"));
291        assert!(display_string.ends_with("seed: None }"));
292    }
293
294    #[test]
295    fn test_walk_params_display_seeded_shows_seed() {
296        let walk_params = WalkParams {
297            size: 10,
298            init_step: Step {
299                x: Xstep::new(
300                    Positive::ONE,
301                    TimeFrame::Day,
302                    ExpirationDate::Days(pos_or_panic!(30.0)),
303                ),
304                y: Ystep::new(0, Positive::HUNDRED),
305            },
306            walk_type: WalkType::Brownian {
307                dt: Positive::ONE,
308                drift: Decimal::ZERO,
309                volatility: pos_or_panic!(0.2),
310            },
311            walker: Box::new(MockWalker),
312            seed: Some(539),
313        };
314        assert!(format!("{walk_params}").ends_with("seed: Some(539) }"));
315    }
316
317    #[test]
318    fn test_with_large_size_positive_types() {
319        let init_x = Xstep::new(
320            Positive::ONE,
321            TimeFrame::Day,
322            ExpirationDate::Days(pos_or_panic!(30.0)),
323        );
324        let init_y = Ystep::new(0, Positive::HUNDRED);
325        let init_step = Step {
326            x: init_x,
327            y: init_y,
328        };
329
330        let size = 1_000_000; // One million steps
331        let walk_params = WalkParams {
332            size,
333            init_step,
334            walk_type: WalkType::Brownian {
335                dt: pos_or_panic!(1.0 / 252.0),
336                drift: dec!(0.0),
337                volatility: pos_or_panic!(0.2),
338            },
339            walker: Box::new(MockWalker),
340            seed: None,
341        };
342
343        assert_eq!(walk_params.size, size);
344        let display_string = format!("{walk_params}");
345        assert!(display_string.contains(&format!("size: {size}")));
346    }
347
348    #[test]
349    fn test_with_different_positive_values() {
350        // Test with smaller and larger positive values
351        let init_x = Xstep::new(
352            pos_or_panic!(0.001), // Very small value
353            TimeFrame::Month,
354            ExpirationDate::Days(pos_or_panic!(90.0)),
355        );
356        let init_y = Ystep::new(0, pos_or_panic!(1000000.0)); // Large value  
357        let init_step = Step {
358            x: init_x,
359            y: init_y,
360        };
361
362        let walk_params = WalkParams {
363            size: 50,
364            init_step,
365            walk_type: WalkType::Brownian {
366                dt: Positive::ONE,
367                drift: Decimal::ZERO,
368                volatility: pos_or_panic!(0.2),
369            },
370            walker: Box::new(MockWalker),
371            seed: None,
372        };
373
374        assert_eq!(walk_params.size, 50);
375        assert_eq!(
376            walk_params.init_step.x.step_size_in_time().value(),
377            pos_or_panic!(0.001).value()
378        );
379        assert_eq!(
380            walk_params.init_step.y.value().value(),
381            pos_or_panic!(1000000.0).value()
382        );
383
384        // Verify the time unit is correctly set
385        assert_eq!(*walk_params.init_step.x.time_unit(), TimeFrame::Month);
386    }
387}