Skip to main content

optionstratlib_simulation/simulation/
randomwalk.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 23/3/25
5******************************************************************************/
6
7use crate::simulation::WalkParams;
8use crate::simulation::steps::Step;
9use optionstratlib_core::model::Positive;
10use optionstratlib_core::utils::Len;
11use optionstratlib_pricing::error::PricingError;
12use optionstratlib_pricing::pricing::Profit;
13use rust_decimal::Decimal;
14use std::fmt::Display;
15use std::ops::{AddAssign, Index, IndexMut};
16
17/// A struct that represents a two-dimensional random walk simulation.
18///
19/// `RandomWalk` stores a sequence of steps that describe a path in a two-dimensional space,
20/// typically used for financial modeling, time series analysis, or statistical simulations.
21/// It maintains both the steps of the random walk and a descriptive title.
22///
23/// # Type Parameters
24///
25/// * `X` - The type for x-axis values (typically representing time or sequence position),
26///   which must implement `AddAssign` (allowing values to be accumulated), be convertible
27///   to `Positive`, and be `Copy`.
28///
29/// * `Y` - The type for y-axis values (typically representing price, value, or position),
30///   which must implement `AddAssign`, be convertible to `Positive`, be `Copy`, and implement
31///   the `Walktypable` trait for additional functionality.
32///
33#[derive(Debug, Clone, Default)]
34pub struct RandomWalk<X, Y>
35where
36    X: Copy + TryInto<Positive> + AddAssign + Display,
37    Y: TryInto<Positive> + Display + Clone,
38{
39    /// The descriptive title of the random walk
40    title: String,
41
42    /// The collection of steps that make up the random walk path
43    steps: Vec<Step<X, Y>>,
44}
45
46impl<X, Y> RandomWalk<X, Y>
47where
48    X: Copy + TryInto<Positive> + AddAssign + Display,
49    Y: TryInto<Positive> + Display + Clone,
50{
51    /// Creates a new random walk instance with the given title and steps.
52    ///
53    /// This constructor takes a title, walk parameters, and a fallible
54    /// generator function that produces the steps of the random walk
55    /// from the provided parameters. Errors from the generator are
56    /// propagated unchanged.
57    ///
58    /// # Parameters
59    ///
60    /// * `title` - A descriptive title for the random walk
61    /// * `params` - Parameters that define the properties of the random walk
62    /// * `generator` - A fallible function that generates the steps of
63    ///   the random walk
64    ///
65    /// # Returns
66    ///
67    /// `Ok(RandomWalk)` on success, or `Err(E)` when the generator fails.
68    ///
69    /// # Errors
70    ///
71    /// Returns the error type produced by the supplied generator. For
72    /// chain-backed generators (e.g. [`crate::simulation::generator_positive`])
73    /// this is [`crate::error::SimulationError`].
74    pub fn new<F, E>(title: String, params: &WalkParams<X, Y>, generator: F) -> Result<Self, E>
75    where
76        F: FnOnce(&WalkParams<X, Y>) -> Result<Vec<Step<X, Y>>, E>,
77        X: Copy + TryInto<Positive> + AddAssign + Display,
78        Y: TryInto<Positive> + Display + Clone,
79    {
80        let steps = generator(params)?;
81        Ok(Self { title, steps })
82    }
83
84    /// Returns the title of the random walk.
85    ///
86    /// # Returns
87    ///
88    /// A string slice containing the title of the random walk.
89    #[must_use]
90    pub fn get_title(&self) -> &str {
91        &self.title
92    }
93
94    /// Updates the title of the random walk.
95    ///
96    /// # Parameters
97    ///
98    /// * `title` - The new title to set
99    pub fn set_title(&mut self, title: String) {
100        self.title = title;
101    }
102
103    /// Returns a vector of references to all steps in the random walk.
104    ///
105    /// # Returns
106    ///
107    /// A vector containing references to all steps in the random walk.
108    #[must_use]
109    pub fn get_steps(&self) -> Vec<&Step<X, Y>> {
110        self.steps.iter().collect::<Vec<&Step<X, Y>>>()
111    }
112
113    /// Returns a reference to the step at the specified index.
114    ///
115    /// # Parameters
116    ///
117    /// * `index` - The zero-based index of the step to retrieve
118    ///
119    /// # Returns
120    ///
121    /// * `Some(&Step<X, Y>)` - The step at `index`.
122    /// * `None` - If `index` is out of bounds.
123    ///
124    /// Named `get_*`, so it follows [`slice::get`] rather than
125    /// [`std::ops::Index`]: an out-of-bounds index is reported, not aborted
126    /// on. Use the `walk[index]` operator when the index is an invariant of
127    /// the caller and a panic is the wanted contract.
128    #[must_use]
129    pub fn get_step(&self, index: usize) -> Option<&Step<X, Y>> {
130        self.steps.get(index)
131    }
132
133    /// Returns a mutable reference to the step at the specified index.
134    ///
135    /// # Parameters
136    ///
137    /// * `index` - The zero-based index of the step to retrieve
138    ///
139    /// # Returns
140    ///
141    /// * `Some(&mut Step<X, Y>)` - The step at `index`.
142    /// * `None` - If `index` is out of bounds.
143    ///
144    /// Mutable counterpart of [`RandomWalk::get_step`], with the same
145    /// out-of-bounds contract.
146    pub fn get_step_mut(&mut self, index: usize) -> Option<&mut Step<X, Y>> {
147        self.steps.get_mut(index)
148    }
149
150    /// Returns a reference to the first step in the random walk, if any.
151    ///
152    /// # Returns
153    ///
154    /// * `Some(&Step<X, Y>)` - A reference to the first step if the random walk is not empty
155    /// * `None` - If the random walk has no steps
156    #[must_use]
157    pub fn first(&self) -> Option<&Step<X, Y>> {
158        self.steps.first()
159    }
160
161    /// Returns a reference to the last step in the random walk, if any.
162    ///
163    /// # Returns
164    ///
165    /// * `Some(&Step<X, Y>)` - A reference to the last step if the random walk is not empty
166    /// * `None` - If the random walk has no steps
167    #[must_use]
168    pub fn last(&self) -> Option<&Step<X, Y>> {
169        self.steps.last()
170    }
171}
172
173/// Implementation of the `Len` trait for `RandomWalk<X, Y>`.
174///
175/// This implementation provides methods to determine the length and emptiness
176/// of a random walk by delegating to the underlying `steps` collection.
177///
178/// # Type Parameters
179///
180/// * `X` - The type for x-axis values (typically time or sequence position),
181///   which must implement `AddAssign`, be convertible to `Positive`, and be `Copy`.
182///
183/// * `Y` - The type for y-axis values (typically price or value),
184///   which must implement `AddAssign`, be convertible to `Positive`, be `Copy`,
185///   and implement the `Walktypable` trait.
186impl<X, Y> Len for RandomWalk<X, Y>
187where
188    X: Copy + TryInto<Positive> + AddAssign + Display,
189    Y: TryInto<Positive> + Display + Clone,
190{
191    /// Returns the number of steps in the random walk.
192    ///
193    /// # Returns
194    ///
195    /// A `usize` representing the number of steps.
196    fn len(&self) -> usize {
197        self.steps.len()
198    }
199
200    /// Determines whether the random walk contains any steps.
201    ///
202    /// # Returns
203    ///
204    /// `true` if the random walk has no steps, `false` otherwise.
205    fn is_empty(&self) -> bool {
206        self.steps.is_empty()
207    }
208}
209
210/// Implementation of the `Index` trait for `RandomWalk<X, Y>`.
211///
212/// This allows accessing the steps of a random walk using array indexing notation:
213/// `walk[index]`.
214///
215/// # Type Parameters
216///
217/// * `X` - The type for x-axis values, with constraints as described above.
218/// * `Y` - The type for y-axis values, with constraints as described above.
219impl<X, Y> Index<usize> for RandomWalk<X, Y>
220where
221    X: Copy + TryInto<Positive> + AddAssign + Display,
222    Y: TryInto<Positive> + Display + Clone,
223{
224    /// The type returned when indexing the random walk.
225    type Output = Step<X, Y>;
226
227    /// Provides read access to a specific step in the random walk by index.
228    ///
229    /// # Parameters
230    ///
231    /// * `index` - The zero-based index of the step to access.
232    ///
233    /// # Returns
234    ///
235    /// A reference to the `Step<X, Y>` at the specified index.
236    ///
237    /// # Panics
238    ///
239    /// Panics if the index is out of bounds, as indexing a `Vec` does. Use
240    /// [`RandomWalk::get_step`] for the checked form, which returns `None`
241    /// instead.
242    #[allow(clippy::indexing_slicing)]
243    fn index(&self, index: usize) -> &Self::Output {
244        // `Index` / `IndexMut` follow the std contract: an out-of-bounds index
245        // is a caller bug and panics, exactly like `Vec`. The checked
246        // accessor is `get_step`, which returns `None` instead (#341, #788).
247        &self.steps[index]
248    }
249}
250
251/// Implementation of the `IndexMut` trait for `RandomWalk<X, Y>`.
252///
253/// This allows modifying steps in a random walk using array indexing notation:
254/// `walk[index] = new_step`.
255///
256/// # Type Parameters
257///
258/// * `X` - The type for x-axis values, with constraints as described above.
259/// * `Y` - The type for y-axis values, with constraints as described above.
260impl<X, Y> IndexMut<usize> for RandomWalk<X, Y>
261where
262    X: Copy + TryInto<Positive> + AddAssign + Display,
263    Y: TryInto<Positive> + Display + Clone,
264{
265    /// Provides mutable access to a specific step in the random walk by index.
266    ///
267    /// # Parameters
268    ///
269    /// * `index` - The zero-based index of the step to modify.
270    ///
271    /// # Returns
272    ///
273    /// A mutable reference to the `Step<X, Y>` at the specified index.
274    ///
275    /// # Panics
276    ///
277    /// Panics if the index is out of bounds, as indexing a `Vec` does. Use
278    /// [`RandomWalk::get_step_mut`] for the checked form, which returns `None`
279    /// instead.
280    #[allow(clippy::indexing_slicing)]
281    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
282        // `Index` / `IndexMut` follow the std contract: an out-of-bounds index
283        // is a caller bug and panics, exactly like `Vec`. The checked
284        // accessor is `get_step_mut`, which returns `None` instead (#341, #788).
285        &mut self.steps[index]
286    }
287}
288
289impl<X, Y> Display for RandomWalk<X, Y>
290where
291    X: Copy + TryInto<Positive> + AddAssign + Display,
292    Y: TryInto<Positive> + Display + Clone,
293{
294    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295        writeln!(f, "RandomWalk Title: {}, Steps:  ", self.title)?;
296        for step in &self.steps {
297            write!(f, "\t{step}")?;
298        }
299        Ok(())
300    }
301}
302
303impl<X, Y> Profit for RandomWalk<X, Y>
304where
305    X: AddAssign + Copy + Display + TryInto<Positive>,
306    Y: TryInto<Positive> + Display + Clone,
307{
308    fn calculate_profit_at(&self, _price: &Positive) -> Result<Decimal, PricingError> {
309        Err(PricingError::other(
310            "Profit calculation not implemented for RandomWalk",
311        ))
312    }
313}
314
315#[cfg(test)]
316#[allow(irrefutable_let_patterns)]
317mod tests_random_walk {
318    use super::*;
319    use optionstratlib_core::model::ExpirationDate;
320
321    use crate::error::SimulationError;
322
323    use crate::simulation::WalkParams;
324    use crate::simulation::WalkType;
325    use crate::simulation::WalkTypeAble;
326    use crate::simulation::steps::Step;
327    use optionstratlib_core::utils::TimeFrame;
328
329    use num_traits::ToPrimitive;
330    use rust_decimal::Decimal;
331
332    use optionstratlib_core::pos_or_panic;
333    use std::convert::Infallible;
334    use std::fmt::Display;
335    use std::ops::AddAssign;
336
337    // Mock implementation of WalkTypeAble for testing
338    #[derive(Clone)]
339    struct TestWalker {}
340
341    impl<X, Y> WalkTypeAble<X, Y> for TestWalker
342    where
343        X: Copy + TryInto<Positive> + AddAssign + Display,
344        Y: TryInto<Positive> + Display + Clone,
345    {
346        // We'll implement the simplest possible method for testing
347        fn brownian(&self, params: &WalkParams<X, Y>) -> Result<Vec<Positive>, SimulationError> {
348            let mut values = Vec::new();
349            let init_value: Positive = params.ystep_as_positive()?;
350            values.push(init_value);
351
352            // Generate some simple steps for test purposes
353            for i in 1..params.size {
354                values.push(pos_or_panic!(
355                    init_value.value().to_f64().unwrap() + i as f64
356                ));
357            }
358
359            Ok(values)
360        }
361    }
362
363    // Helper function to create a walk parameters struct for testing
364    fn create_test_params<X, Y>(
365        size: usize,
366        x_value: X,
367        y_value: Y,
368        walk_type: WalkType,
369    ) -> WalkParams<X, Y>
370    where
371        X: Copy + TryInto<Positive> + AddAssign + Display,
372        Y: TryInto<Positive> + Display + Clone,
373    {
374        let init_step = Step::new(
375            x_value,
376            TimeFrame::Day,
377            ExpirationDate::Days(pos_or_panic!(30.0)),
378            y_value,
379        );
380
381        WalkParams {
382            size,
383            init_step,
384            walk_type,
385            walker: Box::new(TestWalker {}),
386            seed: None,
387        }
388    }
389
390    // Helper function to generate test steps for a random walk
391    fn generate_test_steps<X, Y>(params: &WalkParams<X, Y>) -> Result<Vec<Step<X, Y>>, Infallible>
392    where
393        X: Copy + TryInto<Positive> + AddAssign + Display,
394        Y: TryInto<Positive> + Display + Clone,
395    {
396        let mut steps = Vec::new();
397        steps.push(params.init_step.clone());
398
399        let test_walker = TestWalker {};
400        let values = test_walker
401            .brownian(params)
402            .expect("test brownian generator");
403
404        let mut current_step = params.init_step.clone();
405
406        // Skip the first value as it's the initial step
407        for _value in values.iter().skip(1) {
408            // Convert Positive back to Y type (for test we'll just use the same value)
409            let new_y_value = current_step.y.value();
410
411            // Create next step
412            let next_step = current_step
413                .next(new_y_value.clone())
414                .expect("test step.next");
415            steps.push(next_step.clone());
416
417            current_step = next_step;
418        }
419
420        Ok(steps)
421    }
422
423    #[test]
424    fn test_random_walk_creation() {
425        let params = create_test_params(
426            5,
427            1.0,
428            100.0,
429            WalkType::Brownian {
430                dt: Positive::ONE,
431                drift: Decimal::ZERO,
432                volatility: pos_or_panic!(0.2),
433            },
434        );
435
436        let title = "Test Random Walk".to_string();
437        let Ok(walk) = RandomWalk::new(title.clone(), &params, generate_test_steps) else {
438            unreachable!()
439        };
440
441        assert_eq!(walk.get_title(), title);
442        assert_eq!(walk.len(), 5);
443        assert!(!walk.is_empty());
444    }
445
446    #[test]
447    fn test_random_walk_empty() {
448        let params = create_test_params(
449            0,
450            1.0,
451            100.0,
452            WalkType::Brownian {
453                dt: Positive::ONE,
454                drift: Decimal::ZERO,
455                volatility: pos_or_panic!(0.2),
456            },
457        );
458
459        let title = "Empty Walk".to_string();
460        let Ok(walk) = RandomWalk::new(title.clone(), &params, |_| Ok::<_, Infallible>(Vec::new()))
461        else {
462            unreachable!()
463        };
464
465        assert_eq!(walk.get_title(), title);
466        assert_eq!(walk.len(), 0);
467        assert!(walk.is_empty());
468        assert!(walk.first().is_none());
469        assert!(walk.last().is_none());
470    }
471
472    #[test]
473    fn test_random_walk_title_update() {
474        let params = create_test_params(
475            5,
476            1.0,
477            100.0,
478            WalkType::Brownian {
479                dt: Positive::ONE,
480                drift: Decimal::ZERO,
481                volatility: pos_or_panic!(0.2),
482            },
483        );
484
485        let title = "Initial Title".to_string();
486        let Ok(mut walk) = RandomWalk::new(title, &params, generate_test_steps) else {
487            unreachable!()
488        };
489
490        let new_title = "Updated Title".to_string();
491        walk.set_title(new_title.clone());
492
493        assert_eq!(walk.get_title(), new_title);
494    }
495
496    #[test]
497    fn test_random_walk_first_last() {
498        let params = create_test_params(
499            5,
500            1.0,
501            100.0,
502            WalkType::Brownian {
503                dt: Positive::ONE,
504                drift: Decimal::ZERO,
505                volatility: pos_or_panic!(0.2),
506            },
507        );
508
509        let Ok(walk) = RandomWalk::new("Test Walk".to_string(), &params, generate_test_steps)
510        else {
511            unreachable!()
512        };
513
514        let first = walk.first().unwrap();
515        let last = walk.last().unwrap();
516
517        assert_eq!(*first.x.index(), 0);
518        assert_eq!(*last.x.index(), 4); // 5 steps, zero-indexed
519    }
520
521    #[test]
522    fn test_random_walk_get_steps() {
523        let params = create_test_params(
524            5,
525            1.0,
526            100.0,
527            WalkType::Brownian {
528                dt: Positive::ONE,
529                drift: Decimal::ZERO,
530                volatility: pos_or_panic!(0.2),
531            },
532        );
533
534        let Ok(walk) = RandomWalk::new("Test Walk".to_string(), &params, generate_test_steps)
535        else {
536            unreachable!()
537        };
538
539        let steps = walk.get_steps();
540        assert_eq!(steps.len(), 5);
541
542        // Test that steps have sequential index values
543        for (i, step) in steps.iter().enumerate() {
544            assert_eq!(*step.x.index(), i as i32);
545        }
546    }
547
548    #[test]
549    fn test_random_walk_get_step() {
550        let params = create_test_params(
551            5,
552            1.0,
553            100.0,
554            WalkType::Brownian {
555                dt: Positive::ONE,
556                drift: Decimal::ZERO,
557                volatility: pos_or_panic!(0.2),
558            },
559        );
560
561        let Ok(walk) = RandomWalk::new("Test Walk".to_string(), &params, generate_test_steps)
562        else {
563            unreachable!()
564        };
565
566        let step_0 = walk.get_step(0).unwrap();
567        let step_3 = walk.get_step(3).unwrap();
568
569        assert_eq!(*step_0.x.index(), 0);
570        assert_eq!(*step_3.x.index(), 3);
571    }
572
573    /// An out-of-bounds index is reported, not aborted on. `get_step` follows
574    /// `slice::get`; the panicking contract lives on the `Index` operator,
575    /// which `test_random_walk_index_operator` covers.
576    #[test]
577    fn test_random_walk_get_step_out_of_bounds() {
578        let params = create_test_params(
579            5,
580            1.0,
581            100.0,
582            WalkType::Brownian {
583                dt: Positive::ONE,
584                drift: Decimal::ZERO,
585                volatility: pos_or_panic!(0.2),
586            },
587        );
588
589        let Ok(mut walk) = RandomWalk::new("Test Walk".to_string(), &params, generate_test_steps)
590        else {
591            unreachable!()
592        };
593
594        assert!(walk.get_step(10).is_none());
595        assert!(walk.get_step_mut(10).is_none());
596    }
597
598    #[test]
599    fn test_random_walk_get_step_mut() {
600        let params = create_test_params(
601            5,
602            1.0,
603            100.0,
604            WalkType::Brownian {
605                dt: Positive::ONE,
606                drift: Decimal::ZERO,
607                volatility: pos_or_panic!(0.2),
608            },
609        );
610
611        let Ok(mut walk) = RandomWalk::new("Test Walk".to_string(), &params, generate_test_steps)
612        else {
613            unreachable!()
614        };
615
616        // Get a mutable reference and verify initial state
617        let step_2 = walk.get_step_mut(2).unwrap();
618        assert_eq!(*step_2.x.index(), 2);
619
620        // Get a new step by calling next on the current step
621        let new_y_value = *step_2.y.value();
622        let new_step = step_2.clone();
623        *step_2 = new_step.next(new_y_value * 2.0).unwrap();
624
625        // Verify the step was updated
626        assert_eq!(*walk.get_step(2).unwrap().x.index(), 3);
627    }
628
629    #[test]
630    fn test_random_walk_index_operator() {
631        let params = create_test_params(
632            5,
633            1.0,
634            100.0,
635            WalkType::Brownian {
636                dt: Positive::ONE,
637                drift: Decimal::ZERO,
638                volatility: pos_or_panic!(0.2),
639            },
640        );
641
642        let Ok(walk) = RandomWalk::new("Test Walk".to_string(), &params, generate_test_steps)
643        else {
644            unreachable!()
645        };
646
647        // Test read access via index operator
648        let step_1 = &walk[1];
649        assert_eq!(*step_1.x.index(), 1);
650
651        // Test comparison between get_step and index operator
652        assert_eq!(*walk.get_step(3).unwrap().x.index(), *walk[3].x.index());
653    }
654
655    #[test]
656    fn test_random_walk_index_mut_operator() {
657        let params = create_test_params(
658            5,
659            1.0,
660            100.0,
661            WalkType::Brownian {
662                dt: Positive::ONE,
663                drift: Decimal::ZERO,
664                volatility: pos_or_panic!(0.2),
665            },
666        );
667
668        let Ok(mut walk) = RandomWalk::new("Test Walk".to_string(), &params, generate_test_steps)
669        else {
670            unreachable!()
671        };
672
673        // Get initial step via index
674        let initial_index = *walk[2].x.index();
675
676        // Modify step via index_mut operator
677        let new_y_value = *walk[2].y.value();
678        let new_step = walk[2].clone();
679        walk[2] = new_step.next(new_y_value * 2.0).unwrap();
680
681        // Verify the change
682        assert_ne!(*walk[2].x.index(), initial_index);
683    }
684
685    #[test]
686    fn test_random_walk_new_propagates_generator_error() {
687        // Regression for #349: ensure the fallible generator's error is
688        // surfaced unchanged to the caller.
689        #[derive(Debug, PartialEq)]
690        struct FakeError(&'static str);
691
692        let params = create_test_params(
693            3,
694            1.0_f64,
695            100.0_f64,
696            WalkType::Brownian {
697                dt: Positive::ONE,
698                drift: Decimal::ZERO,
699                volatility: pos_or_panic!(0.2),
700            },
701        );
702
703        let result: Result<RandomWalk<f64, f64>, FakeError> =
704            RandomWalk::new("err".to_string(), &params, |_| Err(FakeError("boom")));
705
706        match result {
707            Err(FakeError(msg)) => assert_eq!(msg, "boom"),
708            Ok(_) => panic!("expected generator error to propagate"),
709        }
710    }
711
712    #[test]
713    fn test_random_walk_display() {
714        let params = create_test_params(
715            3,
716            1.0,
717            100.0,
718            WalkType::Brownian {
719                dt: Positive::ONE,
720                drift: Decimal::ZERO,
721                volatility: pos_or_panic!(0.2),
722            },
723        );
724
725        let Ok(walk) = RandomWalk::new("Display Test".to_string(), &params, generate_test_steps)
726        else {
727            unreachable!()
728        };
729
730        // Test that the display output contains the title
731        let display_output = format!("{walk}");
732        assert!(display_output.contains("Display Test"));
733    }
734
735    #[test]
736    fn test_random_walk_graph_implementation() {
737        let params = create_test_params(
738            5,
739            1.0,
740            100.0,
741            WalkType::Brownian {
742                dt: Positive::ONE,
743                drift: Decimal::ZERO,
744                volatility: pos_or_panic!(0.2),
745            },
746        );
747
748        let Ok(walk) = RandomWalk::new("Graph Test".to_string(), &params, generate_test_steps)
749        else {
750            unreachable!()
751        };
752
753        // The title the visualization layer charts the walk under.
754        assert_eq!(walk.get_title(), "Graph Test");
755    }
756
757    #[test]
758    fn test_with_different_types() {
759        // Test with custom types for X and Y
760        #[derive(Debug, Copy, Clone, PartialEq)]
761        struct TestX(f64);
762
763        impl Display for TestX {
764            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
765                write!(f, "{}", self.0)
766            }
767        }
768
769        impl AddAssign for TestX {
770            fn add_assign(&mut self, other: Self) {
771                self.0 += other.0;
772            }
773        }
774
775        impl From<TestX> for Positive {
776            fn from(val: TestX) -> Self {
777                pos_or_panic!(val.0)
778            }
779        }
780
781        #[derive(Debug, Copy, Clone, PartialEq)]
782        struct TestY(f64);
783
784        impl Display for TestY {
785            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
786                write!(f, "{}", self.0)
787            }
788        }
789
790        impl From<TestY> for Positive {
791            fn from(val: TestY) -> Self {
792                pos_or_panic!(val.0)
793            }
794        }
795
796        // Create params with custom types
797        let params = create_test_params(
798            3,
799            TestX(1.0),
800            TestY(100.0),
801            WalkType::Brownian {
802                dt: Positive::ONE,
803                drift: Decimal::ZERO,
804                volatility: pos_or_panic!(0.2),
805            },
806        );
807
808        // Custom generator for TestX and TestY
809        let generator = |params: &WalkParams<TestX, TestY>| -> Result<_, Infallible> {
810            let mut steps = Vec::new();
811            steps.push(params.init_step.clone());
812
813            let mut current_step = params.init_step.clone();
814            for i in 1..params.size {
815                let next_step = current_step
816                    .next(TestY((100.0 + i as f64) * 1.1))
817                    .expect("test step.next");
818                steps.push(next_step.clone());
819                current_step = next_step;
820            }
821
822            Ok(steps)
823        };
824
825        let Ok(walk) = RandomWalk::new("Custom Types Test".to_string(), &params, generator) else {
826            unreachable!()
827        };
828
829        assert_eq!(walk.len(), 3);
830        assert_eq!(*walk[0].y.value(), TestY(100.0));
831    }
832}