Skip to main content

optionstratlib_simulation/simulation/
model.rs

1use optionstratlib_core::model::Positive;
2use optionstratlib_core::utils::TimeFrame;
3use rust_decimal::Decimal;
4use serde::{Deserialize, Serialize};
5use std::fmt::{Display, Formatter};
6
7/// Enum defining different types of random walks
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
10pub enum WalkType {
11    /// Standard Brownian motion (normal increments)
12    Brownian {
13        /// Time step size (fraction of year: daily=1/365, weekly=1/52, etc.)
14        dt: Positive,
15        /// Drift parameter (expected return or growth rate)
16        drift: Decimal,
17        /// Volatility parameter (annualized standard deviation)
18        volatility: Positive,
19    },
20
21    /// Geometric Brownian motion (log-normal increments)
22    GeometricBrownian {
23        /// Time step size (fraction of year: daily=1/365, weekly=1/52, etc.)
24        dt: Positive,
25        /// Drift parameter (expected return or growth rate)
26        drift: Decimal,
27        /// Volatility parameter (annualized standard deviation)
28        volatility: Positive,
29    },
30
31    /// Log-Returns model (simulates directly log-returns instead of prices)
32    LogReturns {
33        /// Time step size (fraction of year: daily=1/365, weekly=1/52, etc.)
34        dt: Positive,
35        /// Expected return (mean of log returns)
36        expected_return: Decimal,
37        /// Volatility parameter (annualized standard deviation of log returns)
38        volatility: Positive,
39        /// Optional autocorrelation parameter (-1 to 1)
40        autocorrelation: Option<Decimal>,
41    },
42
43    /// Mean-reverting process (Ornstein-Uhlenbeck)
44    MeanReverting {
45        /// Time step size (fraction of year: daily=1/365, weekly=1/52, etc.)
46        dt: Positive,
47        /// Volatility parameter (annualized standard deviation)
48        volatility: Positive,
49        /// Mean reversion speed (rate at which process reverts to mean)
50        speed: Positive,
51        /// Long-term mean (equilibrium level)
52        mean: Positive,
53    },
54
55    /// Jump diffusion process (normal increments with occasional jumps)
56    JumpDiffusion {
57        /// Time step size (fraction of year: daily=1/365, weekly=1/52, etc.)
58        dt: Positive,
59        /// Drift parameter (expected return of continuous part)
60        drift: Decimal,
61        /// Volatility parameter (annualized standard deviation of continuous part)
62        volatility: Positive,
63        /// Jump intensity (annual frequency of jumps)
64        intensity: Positive,
65        /// Jump size mean (average jump magnitude)
66        jump_mean: Decimal,
67        /// Jump size volatility (standard deviation of jump size)
68        jump_volatility: Positive,
69    },
70
71    /// GARCH process (time-varying volatility)
72    Garch {
73        /// Time step size (fraction of year: daily=1/365, weekly=1/52, etc.)
74        dt: Positive,
75        /// Drift parameter (expected return)
76        drift: Decimal,
77        /// Initial volatility parameter (starting volatility level)
78        volatility: Positive,
79        /// GARCH alpha parameter (impact of past observations)
80        alpha: Positive,
81        /// GARCH beta parameter (persistence of volatility)
82        beta: Positive,
83    },
84
85    /// Heston model (stochastic volatility)
86    Heston {
87        /// Time step size (fraction of year: daily=1/365, weekly=1/52, etc.)
88        dt: Positive,
89        /// Drift parameter (expected return)
90        drift: Decimal,
91        /// Initial volatility parameter (starting volatility level)
92        volatility: Positive,
93        /// Mean reversion speed of volatility
94        kappa: Positive,
95        /// Long-term variance (equilibrium level of variance)
96        theta: Positive,
97        /// Volatility of volatility (standard deviation of variance process)
98        xi: Positive,
99        /// Correlation between price and volatility processes
100        rho: Decimal,
101    },
102
103    /// Custom process defined by a function
104    Custom {
105        /// Time step size (fraction of year: daily=1/365, weekly=1/52, etc.)
106        dt: Positive,
107        /// Drift parameter (expected change)
108        drift: Decimal,
109        /// Volatility parameter (may be interpreted differently based on custom implementation)
110        volatility: Positive,
111        /// Volatility of Volatility parameter (annualized standard deviation)
112        vov: Positive,
113        /// Mean reversion speed (rate at which process reverts to mean)
114        vol_speed: Positive,
115        /// Long-term mean (equilibrium level)
116        vol_mean: Positive,
117    },
118
119    /// Telegraph process (two-state regime switching model)
120    Telegraph {
121        /// Time step size (fraction of year: daily=1/365, weekly=1/52, etc.)
122        dt: Positive,
123        /// Drift parameter (expected return)
124        drift: Decimal,
125        /// Base volatility parameter (annualized standard deviation)
126        volatility: Positive,
127        /// Transition rate from state -1 to +1 (intensity of upward regime changes)
128        lambda_up: Positive,
129        /// Transition rate from state +1 to -1 (intensity of downward regime changes)
130        lambda_down: Positive,
131        /// Optional volatility multiplier for the +1 state (default: 1.0)
132        vol_multiplier_up: Option<Positive>,
133        /// Optional volatility multiplier for the -1 state (default: 1.0)
134        vol_multiplier_down: Option<Positive>,
135    },
136
137    /// Represents historical price data for a given timeframe.
138    ///
139    /// This encapsulates the historical price data, including the timeframe
140    /// over which the data was collected and a vector of positive price values.
141    /// It is typically used to store and process historical market data for
142    /// financial analysis and simulation purposes.
143    ///
144    /// # Fields
145    ///
146    /// * `timeframe`: The `TimeFrame` over which the historical data is relevant.
147    /// * `prices`: A `Vec` of `Positive` values representing the historical prices.
148    Historical {
149        /// The timeframe of the historical data.
150        timeframe: TimeFrame,
151        /// The vector of positive price values.
152        prices: Vec<Positive>,
153
154        /// A field representing an optional symbol.
155        ///
156        /// This field stores an optional `String` that can represent a symbol.
157        /// It can hold a `Some(String)` value if a symbol is provided, or `None` if no symbol is specified.
158        ///
159        /// # Use Case
160        ///
161        /// This field can be utilized in scenarios where a symbol (e.g., stock ticker, identifier) may or may not be required.
162        symbol: Option<String>,
163    },
164}
165
166/// A simulated walk that exposes the per-step volatility path alongside the
167/// price path.
168///
169/// Produced by [`crate::simulation::WalkTypeAble::generate_with_vol`]. For
170/// stochastic-volatility walk types (`Garch`, `Heston`, `Custom`,
171/// `Telegraph`) and for `Historical` walks, `vols` carries the ANNUALIZED
172/// volatility prevailing at each step, aligned index-by-index with `prices`.
173/// For constant-volatility walk types `vols` is `None` and the constant is
174/// available via [`WalkType::volatility`].
175#[derive(Debug, Clone)]
176pub struct WalkPath {
177    /// The simulated price path; the first element duplicates the walk's
178    /// initial value (same contract as the per-variant walk methods).
179    pub prices: Vec<Positive>,
180
181    /// Annualized per-step volatilities aligned with `prices` (same length),
182    /// or `None` when the walk type's volatility is constant.
183    pub vols: Option<Vec<Positive>>,
184}
185
186impl WalkType {
187    /// Returns the volatility parameter carried by this walk type.
188    ///
189    /// For the nine synthetic walk types this is the annualized `volatility`
190    /// field of the variant (the initial volatility for stochastic-volatility
191    /// models such as `Garch`, `Heston`, `Custom` and `Telegraph`).
192    ///
193    /// Returns `None` for [`WalkType::Historical`], whose volatility is not a
194    /// parameter but must be estimated from the provided price history.
195    #[must_use]
196    pub fn volatility(&self) -> Option<Positive> {
197        match self {
198            WalkType::Brownian { volatility, .. }
199            | WalkType::GeometricBrownian { volatility, .. }
200            | WalkType::LogReturns { volatility, .. }
201            | WalkType::MeanReverting { volatility, .. }
202            | WalkType::JumpDiffusion { volatility, .. }
203            | WalkType::Garch { volatility, .. }
204            | WalkType::Heston { volatility, .. }
205            | WalkType::Custom { volatility, .. }
206            | WalkType::Telegraph { volatility, .. } => Some(*volatility),
207            WalkType::Historical { .. } => None,
208        }
209    }
210}
211
212impl Display for WalkType {
213    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
214        match self {
215            WalkType::Brownian {
216                dt,
217                drift,
218                volatility,
219            } => write!(
220                f,
221                "Brownian {{ dt: {dt}, drift: {drift}, volatility: {volatility} }}"
222            ),
223            WalkType::GeometricBrownian {
224                dt,
225                drift,
226                volatility,
227            } => write!(
228                f,
229                "GeometricBrownian {{ dt: {dt}, drift: {drift}, volatility: {volatility} }}"
230            ),
231            WalkType::LogReturns {
232                dt,
233                expected_return,
234                volatility,
235                autocorrelation,
236            } => write!(
237                f,
238                "LogReturns {{ dt: {dt}, expected_return: {expected_return}, volatility: {volatility}, autocorrelation: {autocorrelation:?} }}"
239            ),
240            WalkType::MeanReverting {
241                dt,
242                volatility,
243                speed,
244                mean,
245            } => write!(
246                f,
247                "MeanReverting {{ dt: {dt}, volatility: {volatility}, speed: {speed}, mean: {mean} }}"
248            ),
249            WalkType::JumpDiffusion {
250                dt,
251                drift,
252                volatility,
253                intensity,
254                jump_mean,
255                jump_volatility,
256            } => write!(
257                f,
258                "JumpDiffusion {{ dt: {dt}, drift: {drift}, volatility: {volatility}, intensity: {intensity}, jump_mean: {jump_mean}, jump_volatility: {jump_volatility} }}"
259            ),
260            WalkType::Garch {
261                dt,
262                drift,
263                volatility,
264                alpha,
265                beta,
266            } => write!(
267                f,
268                "Garch {{ dt: {dt}, drift: {drift}, volatility: {volatility}, alpha: {alpha}, beta: {beta} }}"
269            ),
270            WalkType::Heston {
271                dt,
272                drift,
273                volatility,
274                kappa,
275                theta,
276                xi,
277                rho,
278            } => write!(
279                f,
280                "Heston {{ dt: {dt}, drift: {drift}, volatility: {volatility}, kappa: {kappa}, theta: {theta}, xi: {xi}, rho: {rho} }}"
281            ),
282            WalkType::Custom {
283                dt,
284                drift,
285                volatility,
286                vov,
287                vol_speed,
288                vol_mean,
289            } => write!(
290                f,
291                "Custom {{ dt: {dt}, drift: {drift}, volatility: {volatility}, vov: {vov}, vol_speed: {vol_speed}, vol_mean: {vol_mean} }}"
292            ),
293            WalkType::Telegraph {
294                dt,
295                drift,
296                volatility,
297                lambda_up,
298                lambda_down,
299                vol_multiplier_up,
300                vol_multiplier_down,
301            } => write!(
302                f,
303                "Telegraph {{ dt: {dt}, drift: {drift}, volatility: {volatility}, lambda_up: {lambda_up}, lambda_down: {lambda_down}, vol_multiplier_up: {vol_multiplier_up:?}, vol_multiplier_down: {vol_multiplier_down:?} }}"
304            ),
305            WalkType::Historical {
306                timeframe,
307                prices,
308                symbol,
309            } => write!(
310                f,
311                "Historical {{ timeframe: {timeframe}, prices: {prices:?}, symbol: {symbol:?} }}"
312            ),
313        }
314    }
315}
316
317#[cfg(test)]
318mod tests_walk_type {
319    use super::*;
320    use optionstratlib_core::pos_or_panic;
321
322    use rust_decimal_macros::dec;
323
324    #[test]
325    fn test_brownian_creation() {
326        let walk = WalkType::Brownian {
327            dt: pos_or_panic!(0.01),
328            drift: dec!(0.05),
329            volatility: pos_or_panic!(0.2),
330        };
331
332        if let WalkType::Brownian {
333            dt,
334            drift,
335            volatility,
336        } = walk
337        {
338            assert_eq!(dt, pos_or_panic!(0.01));
339            assert_eq!(drift, dec!(0.05));
340            assert_eq!(volatility, pos_or_panic!(0.2));
341        } else {
342            panic!("Expected Brownian variant");
343        }
344    }
345
346    #[test]
347    fn test_geometric_brownian_creation() {
348        let walk = WalkType::GeometricBrownian {
349            dt: pos_or_panic!(0.01),
350            drift: dec!(0.05),
351            volatility: pos_or_panic!(0.2),
352        };
353
354        if let WalkType::GeometricBrownian {
355            dt,
356            drift,
357            volatility,
358        } = walk
359        {
360            assert_eq!(dt, pos_or_panic!(0.01));
361            assert_eq!(drift, dec!(0.05));
362            assert_eq!(volatility, pos_or_panic!(0.2));
363        } else {
364            panic!("Expected GeometricBrownian variant");
365        }
366    }
367
368    #[test]
369    fn test_log_returns_creation() {
370        let walk = WalkType::LogReturns {
371            dt: pos_or_panic!(0.01),
372            expected_return: dec!(0.05),
373            volatility: pos_or_panic!(0.2),
374            autocorrelation: Some(dec!(0.1)),
375        };
376
377        if let WalkType::LogReturns {
378            dt,
379            expected_return,
380            volatility,
381            autocorrelation,
382        } = walk
383        {
384            assert_eq!(dt, pos_or_panic!(0.01));
385            assert_eq!(expected_return, dec!(0.05));
386            assert_eq!(volatility, pos_or_panic!(0.2));
387            assert_eq!(autocorrelation, Some(dec!(0.1)));
388        } else {
389            panic!("Expected LogReturns variant");
390        }
391    }
392
393    #[test]
394    fn test_mean_reverting_creation() {
395        let walk = WalkType::MeanReverting {
396            dt: pos_or_panic!(0.01),
397            volatility: pos_or_panic!(0.2),
398            speed: pos_or_panic!(0.1),
399            mean: Positive::HUNDRED,
400        };
401
402        if let WalkType::MeanReverting {
403            dt,
404            volatility,
405            speed,
406            mean,
407        } = walk
408        {
409            assert_eq!(dt, pos_or_panic!(0.01));
410            assert_eq!(volatility, pos_or_panic!(0.2));
411            assert_eq!(speed, pos_or_panic!(0.1));
412            assert_eq!(mean, Positive::HUNDRED);
413        } else {
414            panic!("Expected MeanReverting variant");
415        }
416    }
417
418    #[test]
419    fn test_jump_diffusion_creation() {
420        let walk = WalkType::JumpDiffusion {
421            dt: pos_or_panic!(0.01),
422            drift: dec!(0.05),
423            volatility: pos_or_panic!(0.2),
424            intensity: Positive::ONE,
425            jump_mean: dec!(-0.05),
426            jump_volatility: pos_or_panic!(0.3),
427        };
428
429        if let WalkType::JumpDiffusion {
430            dt,
431            drift,
432            volatility,
433            intensity,
434            jump_mean,
435            jump_volatility,
436        } = walk
437        {
438            assert_eq!(dt, pos_or_panic!(0.01));
439            assert_eq!(drift, dec!(0.05));
440            assert_eq!(volatility, pos_or_panic!(0.2));
441            assert_eq!(intensity, Positive::ONE);
442            assert_eq!(jump_mean, dec!(-0.05));
443            assert_eq!(jump_volatility, pos_or_panic!(0.3));
444        } else {
445            panic!("Expected JumpDiffusion variant");
446        }
447    }
448
449    #[test]
450    fn test_garch_creation() {
451        let walk = WalkType::Garch {
452            dt: pos_or_panic!(0.01),
453            drift: dec!(0.05),
454            volatility: pos_or_panic!(0.2),
455            alpha: pos_or_panic!(0.1),
456            beta: pos_or_panic!(0.8),
457        };
458
459        if let WalkType::Garch {
460            dt,
461            drift,
462            volatility,
463            alpha,
464            beta,
465        } = walk
466        {
467            assert_eq!(dt, pos_or_panic!(0.01));
468            assert_eq!(drift, dec!(0.05));
469            assert_eq!(volatility, pos_or_panic!(0.2));
470            assert_eq!(alpha, pos_or_panic!(0.1));
471            assert_eq!(beta, pos_or_panic!(0.8));
472        } else {
473            panic!("Expected Garch variant");
474        }
475    }
476
477    #[test]
478    fn test_heston_creation() {
479        let walk = WalkType::Heston {
480            dt: pos_or_panic!(0.01),
481            drift: dec!(0.05),
482            volatility: pos_or_panic!(0.2),
483            kappa: Positive::TWO,
484            theta: pos_or_panic!(0.04),
485            xi: pos_or_panic!(0.3),
486            rho: dec!(-0.7),
487        };
488
489        if let WalkType::Heston {
490            dt,
491            drift,
492            volatility,
493            kappa,
494            theta,
495            xi,
496            rho,
497        } = walk
498        {
499            assert_eq!(dt, pos_or_panic!(0.01));
500            assert_eq!(drift, dec!(0.05));
501            assert_eq!(volatility, pos_or_panic!(0.2));
502            assert_eq!(kappa, Positive::TWO);
503            assert_eq!(theta, pos_or_panic!(0.04));
504            assert_eq!(xi, pos_or_panic!(0.3));
505            assert_eq!(rho, dec!(-0.7));
506        } else {
507            panic!("Expected Heston variant");
508        }
509    }
510
511    #[test]
512    fn test_custom_creation() {
513        let walk = WalkType::Custom {
514            dt: pos_or_panic!(0.01),
515            drift: dec!(0.05),
516            volatility: pos_or_panic!(0.2),
517            vov: pos_or_panic!(0.25),
518            vol_speed: pos_or_panic!(0.15),
519            vol_mean: pos_or_panic!(0.3),
520        };
521
522        if let WalkType::Custom {
523            dt,
524            drift,
525            volatility,
526            vov,
527            vol_speed,
528            vol_mean,
529        } = walk
530        {
531            assert_eq!(dt, pos_or_panic!(0.01));
532            assert_eq!(drift, dec!(0.05));
533            assert_eq!(volatility, pos_or_panic!(0.2));
534            assert_eq!(vov, pos_or_panic!(0.25));
535            assert_eq!(vol_speed, pos_or_panic!(0.15));
536            assert_eq!(vol_mean, pos_or_panic!(0.3));
537        } else {
538            panic!("Expected Custom variant");
539        }
540    }
541
542    #[test]
543    fn test_telegraph_creation() {
544        let walk = WalkType::Telegraph {
545            dt: pos_or_panic!(0.01),
546            drift: dec!(0.05),
547            volatility: pos_or_panic!(0.2),
548            lambda_up: pos_or_panic!(0.5),
549            lambda_down: pos_or_panic!(0.3),
550            vol_multiplier_up: Some(pos_or_panic!(1.2)),
551            vol_multiplier_down: Some(pos_or_panic!(0.8)),
552        };
553
554        if let WalkType::Telegraph {
555            dt,
556            drift,
557            volatility,
558            lambda_up,
559            lambda_down,
560            vol_multiplier_up,
561            vol_multiplier_down,
562        } = walk
563        {
564            assert_eq!(dt, pos_or_panic!(0.01));
565            assert_eq!(drift, dec!(0.05));
566            assert_eq!(volatility, pos_or_panic!(0.2));
567            assert_eq!(lambda_up, pos_or_panic!(0.5));
568            assert_eq!(lambda_down, pos_or_panic!(0.3));
569            assert_eq!(vol_multiplier_up, Some(pos_or_panic!(1.2)));
570            assert_eq!(vol_multiplier_down, Some(pos_or_panic!(0.8)));
571        } else {
572            panic!("Expected Telegraph variant");
573        }
574    }
575
576    #[test]
577    fn test_display_brownian() {
578        let walk = WalkType::Brownian {
579            dt: pos_or_panic!(0.01),
580            drift: dec!(0.05),
581            volatility: pos_or_panic!(0.2),
582        };
583
584        let display = format!("{walk}");
585        assert!(display.contains("Brownian"));
586        assert!(display.contains("dt: 0.01"));
587        assert!(display.contains("drift: 0.05"));
588        assert!(display.contains("volatility: 0.2"));
589    }
590
591    #[test]
592    fn test_display_geometric_brownian() {
593        let walk = WalkType::GeometricBrownian {
594            dt: pos_or_panic!(0.01),
595            drift: dec!(0.05),
596            volatility: pos_or_panic!(0.2),
597        };
598
599        let display = format!("{walk}");
600        assert!(display.contains("GeometricBrownian"));
601        assert!(display.contains("dt: 0.01"));
602        assert!(display.contains("drift: 0.05"));
603        assert!(display.contains("volatility: 0.2"));
604    }
605
606    #[test]
607    fn test_display_log_returns() {
608        let walk = WalkType::LogReturns {
609            dt: pos_or_panic!(0.01),
610            expected_return: dec!(0.05),
611            volatility: pos_or_panic!(0.2),
612            autocorrelation: Some(dec!(0.1)),
613        };
614
615        let display = format!("{walk}");
616        assert!(display.contains("LogReturns"));
617        assert!(display.contains("dt: 0.01"));
618        assert!(display.contains("expected_return: 0.05"));
619        assert!(display.contains("volatility: 0.2"));
620        assert!(display.contains("autocorrelation: Some(0.1)"));
621    }
622
623    #[test]
624    fn test_display_mean_reverting() {
625        let walk = WalkType::MeanReverting {
626            dt: pos_or_panic!(0.01),
627            volatility: pos_or_panic!(0.2),
628            speed: pos_or_panic!(0.1),
629            mean: Positive::HUNDRED,
630        };
631
632        let display = format!("{walk}");
633        assert!(display.contains("MeanReverting"));
634        assert!(display.contains("dt: 0.01"));
635        assert!(display.contains("volatility: 0.2"));
636        assert!(display.contains("speed: 0.1"));
637        assert!(display.contains("mean: 100"));
638    }
639
640    #[test]
641    fn test_display_jump_diffusion() {
642        let walk = WalkType::JumpDiffusion {
643            dt: pos_or_panic!(0.01),
644            drift: dec!(0.05),
645            volatility: pos_or_panic!(0.2),
646            intensity: Positive::ONE,
647            jump_mean: dec!(-0.05),
648            jump_volatility: pos_or_panic!(0.3),
649        };
650
651        let display = format!("{walk}");
652        assert!(display.contains("JumpDiffusion"));
653        assert!(display.contains("dt: 0.01"));
654        assert!(display.contains("drift: 0.05"));
655        assert!(display.contains("volatility: 0.2"));
656        assert!(display.contains("intensity: 1"));
657        assert!(display.contains("jump_mean: -0.05"));
658        assert!(display.contains("jump_volatility: 0.3"));
659    }
660
661    #[test]
662    fn test_display_garch() {
663        let walk = WalkType::Garch {
664            dt: pos_or_panic!(0.01),
665            drift: dec!(0.05),
666            volatility: pos_or_panic!(0.2),
667            alpha: pos_or_panic!(0.1),
668            beta: pos_or_panic!(0.8),
669        };
670
671        let display = format!("{walk}");
672        assert!(display.contains("Garch"));
673        assert!(display.contains("dt: 0.01"));
674        assert!(display.contains("drift: 0.05"));
675        assert!(display.contains("volatility: 0.2"));
676        assert!(display.contains("alpha: 0.1"));
677        assert!(display.contains("beta: 0.8"));
678    }
679
680    #[test]
681    fn test_display_heston() {
682        let walk = WalkType::Heston {
683            dt: pos_or_panic!(0.01),
684            drift: dec!(0.05),
685            volatility: pos_or_panic!(0.2),
686            kappa: Positive::TWO,
687            theta: pos_or_panic!(0.04),
688            xi: pos_or_panic!(0.3),
689            rho: dec!(-0.7),
690        };
691
692        let display = format!("{walk}");
693        assert!(display.contains("Heston"));
694        assert!(display.contains("dt: 0.01"));
695        assert!(display.contains("drift: 0.05"));
696        assert!(display.contains("volatility: 0.2"));
697        assert!(display.contains("kappa: 2"));
698        assert!(display.contains("theta: 0.04"));
699        assert!(display.contains("xi: 0.3"));
700        assert!(display.contains("rho: -0.7"));
701    }
702
703    #[test]
704    fn test_display_custom() {
705        let walk = WalkType::Custom {
706            dt: pos_or_panic!(0.01),
707            drift: dec!(0.05),
708            volatility: pos_or_panic!(0.2),
709            vov: pos_or_panic!(0.25),
710            vol_speed: pos_or_panic!(0.15),
711            vol_mean: pos_or_panic!(0.3),
712        };
713
714        let display = format!("{walk}");
715        assert!(display.contains("Custom"));
716        assert!(display.contains("dt: 0.01"));
717        assert!(display.contains("drift: 0.05"));
718        assert!(display.contains("volatility: 0.2"));
719        assert!(display.contains("vov: 0.25"));
720        assert!(display.contains("vol_speed: 0.15"));
721        assert!(display.contains("vol_mean: 0.3"));
722    }
723
724    #[test]
725    fn test_display_telegraph() {
726        let walk = WalkType::Telegraph {
727            dt: pos_or_panic!(0.01),
728            drift: dec!(0.05),
729            volatility: pos_or_panic!(0.2),
730            lambda_up: pos_or_panic!(0.5),
731            lambda_down: pos_or_panic!(0.3),
732            vol_multiplier_up: Some(pos_or_panic!(1.2)),
733            vol_multiplier_down: Some(pos_or_panic!(0.8)),
734        };
735
736        let display = format!("{walk}");
737        assert!(display.contains("Telegraph"));
738        assert!(display.contains("dt: 0.01"));
739        assert!(display.contains("drift: 0.05"));
740        assert!(display.contains("volatility: 0.2"));
741        assert!(display.contains("lambda_up: 0.5"));
742        assert!(display.contains("lambda_down: 0.3"));
743        assert!(display.contains("vol_multiplier_up: Some(1.2)"));
744        assert!(display.contains("vol_multiplier_down: Some(0.8)"));
745    }
746
747    #[test]
748    fn test_log_returns_without_autocorrelation() {
749        let walk = WalkType::LogReturns {
750            dt: pos_or_panic!(0.01),
751            expected_return: dec!(0.05),
752            volatility: pos_or_panic!(0.2),
753            autocorrelation: None,
754        };
755
756        if let WalkType::LogReturns {
757            dt,
758            expected_return,
759            volatility,
760            autocorrelation,
761        } = walk
762        {
763            assert_eq!(dt, pos_or_panic!(0.01));
764            assert_eq!(expected_return, dec!(0.05));
765            assert_eq!(volatility, pos_or_panic!(0.2));
766            assert_eq!(autocorrelation, None);
767        } else {
768            panic!("Expected LogReturns variant");
769        }
770
771        let display = format!("{walk}");
772        assert!(display.contains("LogReturns"));
773        assert!(display.contains("autocorrelation: None"));
774    }
775
776    #[test]
777    fn test_clone() {
778        let walk = WalkType::Brownian {
779            dt: pos_or_panic!(0.01),
780            drift: dec!(0.05),
781            volatility: pos_or_panic!(0.2),
782        };
783
784        let cloned = walk;
785
786        if let WalkType::Brownian {
787            dt,
788            drift,
789            volatility,
790        } = cloned
791        {
792            assert_eq!(dt, pos_or_panic!(0.01));
793            assert_eq!(drift, dec!(0.05));
794            assert_eq!(volatility, pos_or_panic!(0.2));
795        } else {
796            panic!("Expected Brownian variant");
797        }
798    }
799
800    #[test]
801    fn test_clone_equality() {
802        let walk = WalkType::Brownian {
803            dt: pos_or_panic!(0.01),
804            drift: dec!(0.05),
805            volatility: pos_or_panic!(0.2),
806        };
807
808        let cloned = walk.clone();
809
810        // Using Debug formatting to compare
811        assert_eq!(format!("{walk:?}"), format!("{:?}", cloned));
812    }
813}
814
815#[cfg(test)]
816mod tests_serialize {
817    use super::*;
818    use optionstratlib_core::pos_or_panic;
819
820    use rust_decimal_macros::dec;
821    use serde_json::{from_str, to_string};
822
823    #[test]
824    fn test_brownian_serialization() {
825        let walk_type = WalkType::Brownian {
826            dt: pos_or_panic!(0.0027), // ~1/365 for daily
827            drift: dec!(0.05),
828            volatility: pos_or_panic!(0.2),
829        };
830
831        let json = to_string(&walk_type).unwrap();
832
833        // Check basic structure and field inclusion
834        assert!(json.contains("\"Brownian\""));
835        assert!(json.contains("\"dt\""));
836        assert!(json.contains("\"drift\""));
837        assert!(json.contains("\"volatility\""));
838
839        // Check specific values
840        assert!(json.contains("0.0027"));
841        assert!(json.contains("0.05"));
842        assert!(json.contains("0.2"));
843
844        // Deserialize and verify
845        let deserialized: WalkType = from_str(&json).unwrap();
846        assert_eq!(walk_type, deserialized);
847    }
848
849    #[test]
850    fn test_geometric_brownian_serialization() {
851        let walk_type = WalkType::GeometricBrownian {
852            dt: pos_or_panic!(0.0192), // ~1/52 for weekly
853            drift: dec!(0.07),
854            volatility: pos_or_panic!(0.25),
855        };
856
857        let json = to_string(&walk_type).unwrap();
858
859        assert!(json.contains("\"GeometricBrownian\""));
860        assert!(json.contains("\"dt\""));
861        assert!(json.contains("\"drift\""));
862        assert!(json.contains("\"volatility\""));
863
864        assert!(json.contains("0.0192"));
865        assert!(json.contains("0.07"));
866        assert!(json.contains("0.25"));
867
868        let deserialized: WalkType = from_str(&json).unwrap();
869        assert_eq!(walk_type, deserialized);
870    }
871
872    #[test]
873    fn test_log_returns_serialization() {
874        let walk_type = WalkType::LogReturns {
875            dt: pos_or_panic!(0.0833), // ~1/12 for monthly
876            expected_return: dec!(0.06),
877            volatility: pos_or_panic!(0.18),
878            autocorrelation: Some(dec!(0.1)),
879        };
880
881        let json = to_string(&walk_type).unwrap();
882
883        assert!(json.contains("\"LogReturns\""));
884        assert!(json.contains("\"dt\""));
885        assert!(json.contains("\"expected_return\""));
886        assert!(json.contains("\"volatility\""));
887        assert!(json.contains("\"autocorrelation\""));
888
889        assert!(json.contains("0.0833"));
890        assert!(json.contains("0.06"));
891        assert!(json.contains("0.18"));
892        assert!(json.contains("0.1"));
893
894        let deserialized: WalkType = from_str(&json).unwrap();
895        assert_eq!(walk_type, deserialized);
896    }
897
898    #[test]
899    fn test_log_returns_with_none_autocorrelation() {
900        let walk_type = WalkType::LogReturns {
901            dt: pos_or_panic!(0.0833),
902            expected_return: dec!(0.06),
903            volatility: pos_or_panic!(0.18),
904            autocorrelation: None,
905        };
906
907        let json = to_string(&walk_type).unwrap();
908
909        // Check that autocorrelation is null
910        assert!(json.contains("\"autocorrelation\":null"));
911
912        let deserialized: WalkType = from_str(&json).unwrap();
913        assert_eq!(walk_type, deserialized);
914    }
915
916    #[test]
917    fn test_mean_reverting_serialization() {
918        let walk_type = WalkType::MeanReverting {
919            dt: pos_or_panic!(0.0027),
920            volatility: pos_or_panic!(0.3),
921            speed: pos_or_panic!(0.5),
922            mean: Positive::HUNDRED,
923        };
924
925        let json = to_string(&walk_type).unwrap();
926
927        assert!(json.contains("\"MeanReverting\""));
928        assert!(json.contains("\"dt\""));
929        assert!(json.contains("\"volatility\""));
930        assert!(json.contains("\"speed\""));
931        assert!(json.contains("\"mean\""));
932
933        assert!(json.contains("0.0027"));
934        assert!(json.contains("0.3"));
935        assert!(json.contains("0.5"));
936        assert!(json.contains("100"));
937
938        let deserialized: WalkType = from_str(&json).unwrap();
939        assert_eq!(walk_type, deserialized);
940    }
941
942    #[test]
943    fn test_jump_diffusion_serialization() {
944        let walk_type = WalkType::JumpDiffusion {
945            dt: pos_or_panic!(0.0027),
946            drift: dec!(0.04),
947            volatility: pos_or_panic!(0.15),
948            intensity: pos_or_panic!(3.0), // 3 jumps per year expected
949            jump_mean: dec!(-0.05),        // Negative mean for downward jumps
950            jump_volatility: pos_or_panic!(0.1),
951        };
952
953        let json = to_string(&walk_type).unwrap();
954
955        assert!(json.contains("\"JumpDiffusion\""));
956        assert!(json.contains("\"dt\""));
957        assert!(json.contains("\"drift\""));
958        assert!(json.contains("\"volatility\""));
959        assert!(json.contains("\"intensity\""));
960        assert!(json.contains("\"jump_mean\""));
961        assert!(json.contains("\"jump_volatility\""));
962
963        assert!(json.contains("0.0027"));
964        assert!(json.contains("0.04"));
965        assert!(json.contains("0.15"));
966        assert!(json.contains("3"));
967        assert!(json.contains("-0.05"));
968        assert!(json.contains("0.1"));
969
970        let deserialized: WalkType = from_str(&json).unwrap();
971        assert_eq!(walk_type, deserialized);
972    }
973
974    #[test]
975    fn test_garch_serialization() {
976        let walk_type = WalkType::Garch {
977            dt: pos_or_panic!(0.0027),
978            drift: dec!(0.03),
979            volatility: pos_or_panic!(0.2),
980            alpha: pos_or_panic!(0.1),
981            beta: pos_or_panic!(0.8),
982        };
983
984        let json = to_string(&walk_type).unwrap();
985
986        assert!(json.contains("\"Garch\""));
987        assert!(json.contains("\"dt\""));
988        assert!(json.contains("\"drift\""));
989        assert!(json.contains("\"volatility\""));
990        assert!(json.contains("\"alpha\""));
991        assert!(json.contains("\"beta\""));
992
993        let deserialized: WalkType = from_str(&json).unwrap();
994        assert_eq!(walk_type, deserialized);
995    }
996
997    #[test]
998    fn test_heston_serialization() {
999        let walk_type = WalkType::Heston {
1000            dt: pos_or_panic!(0.0027),
1001            drift: dec!(0.05),
1002            volatility: pos_or_panic!(0.2),
1003            kappa: pos_or_panic!(1.5),
1004            theta: pos_or_panic!(0.04),
1005            xi: pos_or_panic!(0.3),
1006            rho: dec!(-0.7),
1007        };
1008
1009        let json = to_string(&walk_type).unwrap();
1010
1011        assert!(json.contains("\"Heston\""));
1012        assert!(json.contains("\"dt\""));
1013        assert!(json.contains("\"drift\""));
1014        assert!(json.contains("\"volatility\""));
1015        assert!(json.contains("\"kappa\""));
1016        assert!(json.contains("\"theta\""));
1017        assert!(json.contains("\"xi\""));
1018        assert!(json.contains("\"rho\""));
1019
1020        let deserialized: WalkType = from_str(&json).unwrap();
1021        assert_eq!(walk_type, deserialized);
1022    }
1023
1024    #[test]
1025    fn test_custom_serialization() {
1026        let walk_type = WalkType::Custom {
1027            dt: pos_or_panic!(0.0027),
1028            drift: dec!(0.05),
1029            volatility: pos_or_panic!(0.2),
1030            vov: pos_or_panic!(0.1),
1031            vol_speed: pos_or_panic!(0.5),
1032            vol_mean: pos_or_panic!(0.2),
1033        };
1034
1035        let json = to_string(&walk_type).unwrap();
1036
1037        assert!(json.contains("\"Custom\""));
1038        assert!(json.contains("\"dt\""));
1039        assert!(json.contains("\"drift\""));
1040        assert!(json.contains("\"volatility\""));
1041        assert!(json.contains("\"vov\""));
1042        assert!(json.contains("\"vol_speed\""));
1043        assert!(json.contains("\"vol_mean\""));
1044
1045        let deserialized: WalkType = from_str(&json).unwrap();
1046        assert_eq!(walk_type, deserialized);
1047    }
1048
1049    #[test]
1050    fn test_telegraph_serialization() {
1051        let walk_type = WalkType::Telegraph {
1052            dt: pos_or_panic!(0.0027),
1053            drift: dec!(0.05),
1054            volatility: pos_or_panic!(0.2),
1055            lambda_up: pos_or_panic!(0.5),
1056            lambda_down: pos_or_panic!(0.3),
1057            vol_multiplier_up: Some(pos_or_panic!(1.2)),
1058            vol_multiplier_down: Some(pos_or_panic!(0.8)),
1059        };
1060
1061        let json = to_string(&walk_type).unwrap();
1062
1063        assert!(json.contains("\"Telegraph\""));
1064        assert!(json.contains("\"dt\""));
1065        assert!(json.contains("\"drift\""));
1066        assert!(json.contains("\"volatility\""));
1067        assert!(json.contains("\"lambda_up\""));
1068        assert!(json.contains("\"lambda_down\""));
1069        assert!(json.contains("\"vol_multiplier_up\""));
1070        assert!(json.contains("\"vol_multiplier_down\""));
1071
1072        let deserialized: WalkType = from_str(&json).unwrap();
1073        assert_eq!(walk_type, deserialized);
1074    }
1075
1076    #[test]
1077    fn test_historical_serialization() {
1078        let walk_type = WalkType::Historical {
1079            timeframe: TimeFrame::Day,
1080            prices: vec![
1081                Positive::HUNDRED,
1082                pos_or_panic!(101.5),
1083                pos_or_panic!(99.8),
1084                pos_or_panic!(102.3),
1085                pos_or_panic!(103.1),
1086            ],
1087            symbol: None,
1088        };
1089
1090        let json = to_string(&walk_type).unwrap();
1091
1092        assert!(json.contains("\"Historical\""));
1093        assert!(json.contains("\"timeframe\""));
1094        assert!(json.contains("\"prices\""));
1095        assert!(json.contains("Day"));
1096        assert!(json.contains("100"));
1097        assert!(json.contains("101.5"));
1098        assert!(json.contains("99.8"));
1099        assert!(json.contains("102.3"));
1100        assert!(json.contains("103.1"));
1101
1102        let deserialized: WalkType = from_str(&json).unwrap();
1103        assert_eq!(walk_type, deserialized);
1104    }
1105
1106    #[test]
1107    fn test_deserialize_from_json_string() {
1108        let json = r#"{
1109            "GeometricBrownian": {
1110                "dt": 0.0027,
1111                "drift": 0.06,
1112                "volatility": 0.22
1113            }
1114        }"#;
1115
1116        let walk_type: WalkType = from_str(json).unwrap();
1117
1118        match walk_type {
1119            WalkType::GeometricBrownian {
1120                dt,
1121                drift,
1122                volatility,
1123            } => {
1124                assert_eq!(dt, pos_or_panic!(0.0027));
1125                assert_eq!(drift, dec!(0.06));
1126                assert_eq!(volatility, pos_or_panic!(0.22));
1127            }
1128            _ => panic!("Wrong variant deserialized"),
1129        }
1130    }
1131
1132    #[test]
1133    fn test_negative_decimal_values() {
1134        let walk_type = WalkType::JumpDiffusion {
1135            dt: pos_or_panic!(0.0027),
1136            drift: dec!(-0.03), // Negative drift
1137            volatility: pos_or_panic!(0.15),
1138            intensity: Positive::TWO,
1139            jump_mean: dec!(-0.1), // Negative jump mean
1140            jump_volatility: pos_or_panic!(0.05),
1141        };
1142
1143        let json = to_string(&walk_type).unwrap();
1144
1145        assert!(json.contains("-0.03"));
1146        assert!(json.contains("-0.1"));
1147
1148        let deserialized: WalkType = from_str(&json).unwrap();
1149        assert_eq!(walk_type, deserialized);
1150    }
1151}