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}