Skip to main content

stochastic_rs_viz/
lib.rs

1//! # stochastic-rs-viz
2//!
3//! Plotly-based visualization for stochastic processes and distributions.
4//!
5//! Module layout:
6//! - [`plottable`] — `Plottable<T>` trait + impls for the canonical
7//!   `ProcessExt::Output` shapes (1D path, complex path, fixed-arity tuple,
8//!   2D matrix).
9//! - [`grid_plotter`] — `GridPlotter` builder for multi-subplot HTML grids.
10//! - [`convenience`] — one-shot `plot_process` / `plot_distribution` /
11//!   `plot_vol_surface` HTML writers.
12
13#![allow(non_snake_case)]
14#![allow(clippy::type_complexity)]
15#![allow(clippy::too_many_arguments)]
16
17pub mod convenience;
18pub mod grid_plotter;
19pub mod plottable;
20
21pub use convenience::plot_distribution;
22pub use convenience::plot_process;
23pub use convenience::plot_vol_surface;
24pub use grid_plotter::GridPlotter;
25pub use plottable::Plottable;
26
27#[cfg(test)]
28mod tests {
29  use ndarray::Array1;
30  use plotly::Layout;
31  use plotly::Plot;
32  use plotly::Scatter;
33  use plotly::Surface;
34  use plotly::common::DashType;
35  use plotly::common::Mode;
36  use rand_distr::Exp;
37  use rand_distr::Normal;
38  use stochastic_rs_stochastic::autoregressive::agrach::Agarch;
39  use stochastic_rs_stochastic::autoregressive::ar::ARp;
40  use stochastic_rs_stochastic::autoregressive::arch::Arch;
41  use stochastic_rs_stochastic::autoregressive::arima::Arima;
42  use stochastic_rs_stochastic::autoregressive::egarch::Egarch;
43  use stochastic_rs_stochastic::autoregressive::garch::Garch;
44  use stochastic_rs_stochastic::autoregressive::ma::MAq;
45  use stochastic_rs_stochastic::autoregressive::sarima::Sarima;
46  use stochastic_rs_stochastic::autoregressive::tgarch::Tgarch;
47  use stochastic_rs_stochastic::diffusion::cev::Cev;
48  use stochastic_rs_stochastic::diffusion::cfou::Cfou;
49  use stochastic_rs_stochastic::diffusion::cir::Cir as DiffCIR;
50  use stochastic_rs_stochastic::diffusion::fcir::Fcir;
51  use stochastic_rs_stochastic::diffusion::feller::FellerLogistic;
52  use stochastic_rs_stochastic::diffusion::fgbm::Fgbm;
53  use stochastic_rs_stochastic::diffusion::fjacobi::FJacobi;
54  use stochastic_rs_stochastic::diffusion::fou::Fou;
55  use stochastic_rs_stochastic::diffusion::fouque::FouqueOU2D;
56  use stochastic_rs_stochastic::diffusion::gbm::Gbm;
57  use stochastic_rs_stochastic::diffusion::gbm_ih::GbmIh;
58  use stochastic_rs_stochastic::diffusion::gompertz::Gompertz;
59  use stochastic_rs_stochastic::diffusion::jacobi::Jacobi;
60  use stochastic_rs_stochastic::diffusion::kimura::Kimura;
61  use stochastic_rs_stochastic::diffusion::ou::Ou;
62  use stochastic_rs_stochastic::diffusion::quadratic::Quadratic;
63  use stochastic_rs_stochastic::diffusion::verhulst::Verhulst;
64  use stochastic_rs_stochastic::interest::adg::Adg;
65  use stochastic_rs_stochastic::interest::bgm::Bgm;
66  use stochastic_rs_stochastic::interest::cir::Cir as RateCIR;
67  use stochastic_rs_stochastic::interest::cir_2f::Cir2F;
68  use stochastic_rs_stochastic::interest::duffie_kan::DuffieKan;
69  use stochastic_rs_stochastic::interest::duffie_kan_jump_exp::DuffieKanJumpExp;
70  use stochastic_rs_stochastic::interest::fractional_vasicek::FVasicek;
71  use stochastic_rs_stochastic::interest::hjm::Hjm;
72  use stochastic_rs_stochastic::interest::ho_lee::HoLee;
73  use stochastic_rs_stochastic::interest::hull_white::HullWhite;
74  use stochastic_rs_stochastic::interest::hull_white_2f::HullWhite2F;
75  use stochastic_rs_stochastic::interest::vasicek::Vasicek;
76  use stochastic_rs_stochastic::interest::wu_zhang::WuZhangD;
77  use stochastic_rs_stochastic::isonormal::IsoNormal;
78  use stochastic_rs_stochastic::isonormal::fbm_custom_inc_cov;
79  use stochastic_rs_stochastic::jump::bates::Bates1996;
80  use stochastic_rs_stochastic::jump::cgmy::Cgmy;
81  use stochastic_rs_stochastic::jump::cts::Cts;
82  use stochastic_rs_stochastic::jump::ig::Ig;
83  use stochastic_rs_stochastic::jump::jump_fou::JumpFou;
84  use stochastic_rs_stochastic::jump::jump_fou_custom::JumpFOUCustom;
85  use stochastic_rs_stochastic::jump::kobol::KoBoL;
86  use stochastic_rs_stochastic::jump::kou::Kou;
87  use stochastic_rs_stochastic::jump::levy_diffusion::LevyDiffusion;
88  use stochastic_rs_stochastic::jump::merton::Merton;
89  use stochastic_rs_stochastic::jump::nig::Nig;
90  use stochastic_rs_stochastic::jump::rdts::Rdts;
91  use stochastic_rs_stochastic::jump::vg::Vg;
92  use stochastic_rs_stochastic::noise::cfgns::Cfgns;
93  use stochastic_rs_stochastic::noise::cgns::Cgns;
94  use stochastic_rs_stochastic::noise::fgn::Fgn;
95  use stochastic_rs_stochastic::noise::gn::Gn;
96  use stochastic_rs_stochastic::noise::wn::Wn;
97  use stochastic_rs_stochastic::process::bm::Bm;
98  use stochastic_rs_stochastic::process::cbms::Cbms;
99  use stochastic_rs_stochastic::process::ccustom::CompoundCustom;
100  use stochastic_rs_stochastic::process::cfbms::Cfbms;
101  use stochastic_rs_stochastic::process::cpoisson::CompoundPoisson;
102  use stochastic_rs_stochastic::process::customjt::CustomJt;
103  use stochastic_rs_stochastic::process::fbm::Fbm;
104  use stochastic_rs_stochastic::process::lfsm::Lfsm;
105  use stochastic_rs_stochastic::process::poisson::Poisson;
106  use stochastic_rs_stochastic::process::subordinator::AlphaStableSubordinator;
107  use stochastic_rs_stochastic::process::subordinator::Ctrw;
108  use stochastic_rs_stochastic::process::subordinator::CtrwJumpLaw;
109  use stochastic_rs_stochastic::process::subordinator::CtrwWaitingLaw;
110  use stochastic_rs_stochastic::process::subordinator::GammaSubordinator;
111  use stochastic_rs_stochastic::process::subordinator::IGSubordinator;
112  use stochastic_rs_stochastic::process::subordinator::InverseAlphaStableSubordinator;
113  use stochastic_rs_stochastic::process::subordinator::PoissonSubordinator;
114  use stochastic_rs_stochastic::process::subordinator::TemperedStableSubordinator;
115  use stochastic_rs_stochastic::sheet::fbs::Fbs;
116  use stochastic_rs_stochastic::traits::ProcessExt;
117  use stochastic_rs_stochastic::volatility::HestonPow;
118  use stochastic_rs_stochastic::volatility::bergomi::Bergomi;
119  use stochastic_rs_stochastic::volatility::fheston::RoughHeston;
120  use stochastic_rs_stochastic::volatility::heston::Heston;
121  use stochastic_rs_stochastic::volatility::rbergomi::RoughBergomi;
122  use stochastic_rs_stochastic::volatility::sabr::Sabr;
123  use stochastic_rs_stochastic::volatility::svcgmy::Svcgmy;
124
125  use super::*;
126
127  fn f_const_001(_: f64) -> f64 {
128    0.01
129  }
130
131  fn f_const_002(_: f64) -> f64 {
132    0.02
133  }
134
135  fn f_linear_small(t: f64) -> f64 {
136    0.01 + 0.005 * t
137  }
138
139  fn f_phi_small(t: f64) -> f64 {
140    0.002 * t
141  }
142
143  fn f_hjm_p(t: f64, u: f64) -> f64 {
144    0.01 + 0.01 * (u - t).max(0.0)
145  }
146
147  fn f_hjm_q(_: f64, _: f64) -> f64 {
148    0.5
149  }
150
151  fn f_hjm_v(_: f64, _: f64) -> f64 {
152    0.02
153  }
154
155  fn f_hjm_alpha(_: f64, _: f64) -> f64 {
156    0.01
157  }
158
159  fn f_hjm_sigma(_: f64, _: f64) -> f64 {
160    0.015
161  }
162
163  fn f_adg_k(t: f64) -> f64 {
164    0.02 + 0.002 * t
165  }
166
167  fn f_adg_theta(_: f64) -> f64 {
168    0.6
169  }
170
171  fn f_adg_phi(_: f64) -> f64 {
172    0.01
173  }
174
175  fn f_adg_b(_: f64) -> f64 {
176    0.2
177  }
178
179  fn f_adg_c(_: f64) -> f64 {
180    0.05
181  }
182
183  fn normal_cpoisson(lambda: f64, n: usize, jump_sigma: f64) -> CompoundPoisson<f64, Normal<f64>> {
184    CompoundPoisson::new(
185      Normal::new(0.0, jump_sigma).expect("valid normal"),
186      Poisson::new(lambda, Some(n), Some(1.0)),
187    )
188  }
189
190  #[test]
191  fn plot_grid() {
192    let n = 96;
193    let traj = 1;
194    let j = 64;
195    let sheet_m = 3;
196    let sheet_n = 64;
197
198    let mut isonormal_fbm = IsoNormal::new(
199      |aux_idx, idx| fbm_custom_inc_cov(aux_idx.abs_diff(idx), 0.7),
200      (0..n).collect(),
201    );
202    let mut isonormal_paths = Vec::with_capacity(traj);
203    for _ in 0..traj {
204      let increments = isonormal_fbm.get_path();
205      let mut path = Vec::with_capacity(n);
206      path.push(0.0);
207      let mut acc = 0.0;
208      for &dx in &increments {
209        acc += dx;
210        path.push(acc);
211      }
212      isonormal_paths.push(path);
213    }
214
215    let mut grid = GridPlotter::new()
216      .title("Stochastic Processes (Grid)")
217      .cols(4)
218      .show_legend(false)
219      .line_width(1.2)
220      .x_gap(0.80)
221      .y_gap(5.00);
222
223    grid = grid.register(
224      &ARp::new(Array1::from_vec(vec![0.65, -0.2]), 0.08, n, None),
225      "Autoreg: AR(2)",
226      traj,
227    );
228    grid = grid.register(
229      &MAq::new(Array1::from_vec(vec![0.5, -0.2]), 0.1, n),
230      "Autoreg: MA(2)",
231      traj,
232    );
233    grid = grid.register(
234      &Arima::new(
235        Array1::from_vec(vec![0.4]),
236        Array1::from_vec(vec![0.3]),
237        1,
238        0.1,
239        n,
240      ),
241      "Autoreg: Arima(1,1,1)",
242      traj,
243    );
244    grid = grid.register(
245      &Sarima::new(
246        Array1::from_vec(vec![0.3]),
247        Array1::from_vec(vec![0.2]),
248        Array1::from_vec(vec![0.2]),
249        Array1::from_vec(vec![0.1]),
250        1,
251        1,
252        12,
253        0.08,
254        n,
255      ),
256      "Autoreg: Sarima",
257      traj,
258    );
259    grid = grid.register(
260      &Arch::new(0.05, Array1::from_vec(vec![0.2, 0.1]), n),
261      "Autoreg: Arch",
262      traj,
263    );
264    grid = grid.register(
265      &Garch::new(
266        0.03,
267        Array1::from_vec(vec![0.12]),
268        Array1::from_vec(vec![0.8]),
269        n,
270      ),
271      "Autoreg: Garch",
272      traj,
273    );
274    grid = grid.register(
275      &Tgarch::new(
276        0.03,
277        Array1::from_vec(vec![0.08]),
278        Array1::from_vec(vec![0.05]),
279        Array1::from_vec(vec![0.85]),
280        n,
281      ),
282      "Autoreg: Tgarch",
283      traj,
284    );
285    grid = grid.register(
286      &Egarch::new(
287        -0.1,
288        Array1::from_vec(vec![0.1]),
289        Array1::from_vec(vec![-0.05]),
290        Array1::from_vec(vec![0.9]),
291        n,
292      ),
293      "Autoreg: Egarch",
294      traj,
295    );
296    grid = grid.register(
297      &Agarch::new(
298        0.03,
299        Array1::from_vec(vec![0.1]),
300        Array1::from_vec(vec![0.04]),
301        Array1::from_vec(vec![0.84]),
302        n,
303      ),
304      "Autoreg: Agarch",
305      traj,
306    );
307
308    grid = grid.register(&Wn::new(n, Some(0.0), Some(1.0)), "Noise: White", traj);
309    grid = grid.register(&Gn::new(n, Some(1.0)), "Noise: Gaussian", traj);
310    grid = grid.register(&Fgn::new(0.7, n, Some(1.0)), "Noise: Fgn", traj);
311    grid = grid.register(&Cgns::new(-0.4, n, Some(1.0)), "Noise: Cgns", traj);
312    grid = grid.register(&Cfgns::new(0.7, -0.3, n, Some(1.0)), "Noise: Cfgns", traj);
313
314    grid = grid.register(&Bm::new(n, Some(1.0)), "Process: Bm", traj);
315    grid = grid.register(&Fbm::new(0.7, n, Some(1.0)), "Process: Fbm", traj);
316    grid = grid.register_paths(isonormal_paths, "Process: fBM via IsoNormal (H=0.7)");
317    grid = grid.register(
318      &Poisson::new(2.0, Some(n), Some(1.0)),
319      "Process: Poisson",
320      traj,
321    );
322    grid = grid.register(
323      &CustomJt::new(
324        Some(n),
325        Some(1.0),
326        Exp::new(10.0).expect("positive exponential rate"),
327      ),
328      "Process: CustomJt",
329      traj,
330    );
331    grid = grid.register(
332      &CompoundPoisson::new(
333        Normal::new(0.0, 0.15).expect("valid normal"),
334        Poisson::new(1.2, Some(n), Some(1.0)),
335      ),
336      "Process: CompoundPoisson",
337      traj,
338    );
339    grid = grid.register(
340      &CompoundCustom::new(
341        Some(n),
342        Some(1.0),
343        Normal::new(0.0, 0.1).expect("valid normal"),
344        Exp::new(15.0).expect("positive exponential rate"),
345        CustomJt::new(
346          Some(n),
347          Some(1.0),
348          Exp::new(15.0).expect("positive exponential rate"),
349        ),
350      ),
351      "Process: CompoundCustom",
352      traj,
353    );
354    grid = grid.register(&Cbms::new(0.35, n, Some(1.0)), "Process: Cbms", traj);
355    grid = grid.register(&Cfbms::new(0.7, 0.35, n, Some(1.0)), "Process: Cfbms", traj);
356    grid = grid.register(
357      &Lfsm::new(1.7, 0.0, 0.8, 1.0, n, Some(0.0), Some(1.0)),
358      "Process: Lfsm",
359      traj,
360    );
361    grid = grid.register(
362      &AlphaStableSubordinator::new(0.7, 1.0, n, Some(0.0), Some(1.0)),
363      "Process: AlphaStable Subordinator",
364      traj,
365    );
366    grid = grid.register(
367      &InverseAlphaStableSubordinator::new(0.7, 1.0, n, Some(1.0), 2048, Some(4.0)),
368      "Process: Inverse AlphaStable",
369      traj,
370    );
371    grid = grid.register(
372      &PoissonSubordinator::new(2.0, n, Some(0.0), Some(1.0)),
373      "Process: Poisson Subordinator",
374      traj,
375    );
376    grid = grid.register(
377      &GammaSubordinator::new(3.0, 5.0, n, Some(0.0), Some(1.0)),
378      "Process: Gamma Subordinator",
379      traj,
380    );
381    grid = grid.register(
382      &IGSubordinator::new(1.5, 2.0, n, Some(0.0), Some(1.0)),
383      "Process: Ig Subordinator",
384      traj,
385    );
386    grid = grid.register(
387      &TemperedStableSubordinator::new(0.7, 1.0, 2.0, 0.05, n, Some(0.0), Some(1.0)),
388      "Process: Tempered Stable Subordinator",
389      traj,
390    );
391    grid = grid.register(
392      &Ctrw::new(
393        CtrwWaitingLaw::Exponential { rate: 2.0 },
394        CtrwJumpLaw::Normal {
395          mean: 0.0,
396          std: 0.3,
397        },
398        n,
399        Some(0.0),
400        Some(1.0),
401      ),
402      "Process: Ctrw",
403      traj,
404    );
405
406    grid = grid.register(
407      &Ou::new(2.0, 0.0, 0.2, n, Some(0.0), Some(1.0)),
408      "Diffusion: Ou",
409      traj,
410    );
411    grid = grid.register(
412      &Gbm::new(0.05, 0.2, n, Some(100.0), Some(1.0)),
413      "Diffusion: Gbm",
414      traj,
415    );
416    grid = grid.register(
417      &DiffCIR::new(2.5, 0.04, 0.2, n, Some(0.04), Some(1.0), Some(false)),
418      "Diffusion: Cir",
419      traj,
420    );
421    grid = grid.register(
422      &Cev::new(0.04, 0.2, 0.8, n, Some(1.0), Some(1.0)),
423      "Diffusion: Cev",
424      traj,
425    );
426    grid = grid.register(
427      &FellerLogistic::new(2.0, 1.0, 0.3, n, Some(0.5), Some(1.0), Some(false)),
428      "Diffusion: Feller Logistic",
429      traj,
430    );
431    grid = grid.register(
432      &Verhulst::new(1.2, 2.0, 0.2, n, Some(0.5), Some(1.0), Some(true)),
433      "Diffusion: Verhulst",
434      traj,
435    );
436    grid = grid.register(
437      &Gompertz::new(1.0, 0.8, 0.2, n, Some(1.0), Some(1.0)),
438      "Diffusion: Gompertz",
439      traj,
440    );
441    grid = grid.register(
442      &Kimura::new(1.0, 0.3, n, Some(0.4), Some(1.0)),
443      "Diffusion: Kimura",
444      traj,
445    );
446    grid = grid.register(
447      &Quadratic::new(0.1, -0.2, 0.05, 0.15, n, Some(1.0), Some(1.0)),
448      "Diffusion: Quadratic",
449      traj,
450    );
451    grid = grid.register(
452      &Jacobi::new(0.8, 1.4, 0.4, n, Some(0.3), Some(1.0)),
453      "Diffusion: Jacobi",
454      traj,
455    );
456    grid = grid.register(
457      &Fcir::new(0.7, 2.5, 0.04, 0.2, n, Some(0.04), Some(1.0), Some(false)),
458      "Diffusion: Fcir",
459      traj,
460    );
461    grid = grid.register(
462      &FJacobi::new(0.7, 0.8, 1.4, 0.35, n, Some(0.3), Some(1.0)),
463      "Diffusion: FJacobi",
464      traj,
465    );
466    grid = grid.register(
467      &Fou::new(0.7, 2.0, 0.0, 0.2, n, Some(0.0), Some(1.0)),
468      "Diffusion: Fou",
469      traj,
470    );
471    grid = grid.register(
472      &Cfou::new(0.7, 1.8, 3.0, 0.4, n, Some(0.0), Some(0.0), Some(1.0)),
473      "Diffusion: Complex fOU",
474      traj,
475    );
476    grid = grid.register(
477      &Fgbm::new(0.7, 0.04, 0.2, n, Some(100.0), Some(1.0)),
478      "Diffusion: Fgbm",
479      traj,
480    );
481    grid = grid.register(
482      &GbmIh::new(0.04, 0.2, n, Some(100.0), Some(1.0), None),
483      "Diffusion: GbmIh",
484      traj,
485    );
486    grid = grid.register(
487      &FouqueOU2D::new(1.5, 0.0, 0.3, 0.0, n, Some(0.0), Some(0.0), Some(1.0)),
488      "Diffusion: Fouque Ou 2D",
489      traj,
490    );
491
492    grid = grid.register(
493      &Vasicek::new(3.0, 0.03, 0.02, n, Some(0.03), Some(1.0)),
494      "Interest: Vasicek",
495      traj,
496    );
497    grid = grid.register(
498      &FVasicek::new(0.7, 2.0, 0.03, 0.02, n, Some(0.03), Some(1.0)),
499      "Interest: Fractional Vasicek",
500      traj,
501    );
502    grid = grid.register(
503      &RateCIR::new(2.5, 0.04, 0.2, n, Some(0.04), Some(1.0), Some(false)),
504      "Interest: Cir (Alias)",
505      traj,
506    );
507    grid = grid.register(
508      &HoLee::new(None, Some(0.01), 0.01, n, Some(1.0)),
509      "Interest: Ho-Lee",
510      traj,
511    );
512    grid = grid.register(
513      &HullWhite::new(
514        f_linear_small as fn(f64) -> f64,
515        0.4,
516        0.02,
517        n,
518        Some(0.02),
519        Some(1.0),
520      ),
521      "Interest: Hull-White",
522      traj,
523    );
524    grid = grid.register(
525      &HullWhite2F::new(
526        f_const_001 as fn(f64) -> f64,
527        0.5,
528        0.02,
529        0.015,
530        -0.3,
531        0.4,
532        Some(0.02),
533        Some(1.0),
534        n,
535      ),
536      "Interest: Hull-White 2F",
537      traj,
538    );
539    grid = grid.register(
540      &Hjm::new(
541        f_const_001 as fn(f64) -> f64,
542        f_const_002 as fn(f64) -> f64,
543        f_hjm_p as fn(f64, f64) -> f64,
544        f_hjm_q as fn(f64, f64) -> f64,
545        f_hjm_v as fn(f64, f64) -> f64,
546        f_hjm_alpha as fn(f64, f64) -> f64,
547        f_hjm_sigma as fn(f64, f64) -> f64,
548        n,
549        Some(0.01),
550        Some(1.0),
551        Some(0.01),
552        Some(1.0),
553      ),
554      "Interest: Hjm",
555      traj,
556    );
557    grid = grid.register(
558      &Bgm::new(
559        Array1::from_vec(vec![0.2, 0.15]),
560        Array1::from_vec(vec![0.02, 0.025]),
561        2,
562        Some(1.0),
563        n,
564      ),
565      "Interest: Bgm",
566      traj,
567    );
568    grid = grid.register(
569      &Adg::new(
570        f_adg_k as fn(f64) -> f64,
571        f_adg_theta as fn(f64) -> f64,
572        Array1::from_vec(vec![0.02, 0.018]),
573        f_adg_phi as fn(f64) -> f64,
574        f_adg_b as fn(f64) -> f64,
575        f_adg_c as fn(f64) -> f64,
576        n,
577        2,
578        Array1::from_vec(vec![0.01, 0.015]),
579        Some(1.0),
580      ),
581      "Interest: Adg",
582      traj,
583    );
584    grid = grid.register(
585      &DuffieKan::new(
586        0.2,
587        0.1,
588        0.05,
589        -0.3,
590        -0.1,
591        0.2,
592        0.01,
593        0.1,
594        0.15,
595        -0.2,
596        0.01,
597        0.12,
598        n,
599        Some(0.02),
600        Some(0.01),
601        Some(1.0),
602      ),
603      "Interest: Duffie-Kan",
604      traj,
605    );
606    grid = grid.register(
607      &DuffieKanJumpExp::new(
608        0.2,
609        0.1,
610        0.05,
611        -0.3,
612        -0.1,
613        0.2,
614        0.01,
615        0.1,
616        0.15,
617        -0.2,
618        0.01,
619        0.12,
620        2.0,
621        0.02,
622        n,
623        Some(0.02),
624        Some(0.01),
625        Some(1.0),
626      ),
627      "Interest: Duffie-Kan Jump Exp",
628      traj,
629    );
630    grid = grid.register(
631      &WuZhangD::new(
632        Array1::from_vec(vec![0.05, 0.04]),
633        Array1::from_vec(vec![1.2, 1.0]),
634        Array1::from_vec(vec![0.3, 0.25]),
635        Array1::from_vec(vec![0.4, 0.3]),
636        Array1::from_vec(vec![0.02, 0.025]),
637        Array1::from_vec(vec![0.04, 0.03]),
638        2,
639        Some(1.0),
640        n,
641      ),
642      "Interest: Wu-Zhang",
643      traj,
644    );
645    grid = grid.register(
646      &Cir2F::new(
647        RateCIR::new(2.5, 0.03, 0.12, n, Some(0.03), Some(1.0), Some(false)),
648        RateCIR::new(2.0, 0.02, 0.1, n, Some(0.02), Some(1.0), Some(false)),
649        f_phi_small as fn(f64) -> f64,
650      ),
651      "Interest: Cir 2F",
652      traj,
653    );
654
655    grid = grid.register(
656      &Vg::new(0.0, 0.2, 0.15, n, Some(0.0), Some(1.0)),
657      "Jump: Vg",
658      traj,
659    );
660    grid = grid.register(
661      &Nig::new(0.0, 0.2, 0.3, n, Some(0.0), Some(1.0)),
662      "Jump: Nig",
663      traj,
664    );
665    grid = grid.register(&Ig::new(1.0, n, Some(0.0), Some(1.0)), "Jump: Ig", traj);
666    grid = grid.register(
667      &Rdts::new(4.0, 5.0, 0.7, n, j, Some(0.0), Some(1.0)),
668      "Jump: Rdts",
669      traj,
670    );
671    grid = grid.register(
672      &Cts::new(4.0, 5.0, 0.7, n, j, Some(0.0), Some(1.0)),
673      "Jump: Cts",
674      traj,
675    );
676
677    let g = 4.0;
678    let m = 5.0;
679    let y = 0.7;
680
681    let c = Cgmy::<f64>::c_for_unit_variance(g, m, y);
682    // KoBoL: in case of p=q=1 D_for_unit_variance == C_for_unit_variance
683    let d = KoBoL::<f64>::d_for_unit_variance(1.0, 1.0, g, m, y);
684
685    grid = grid.register(
686      &Cgmy::<f64>::new(c, g, m, y, n, j, Some(0.0), Some(1.0)),
687      "Jump: Cgmy (unit var, symmetric)",
688      traj,
689    );
690
691    grid = grid.register(
692      &KoBoL::<f64>::new(d, 1.0, 1.0, g, m, y, n, j, Some(0.0), Some(1.0)),
693      "Jump: KoBoL (unit var, p=q=1)",
694      traj,
695    );
696
697    grid = grid.register(
698      &Merton::new(
699        0.03,
700        0.2,
701        1.0,
702        0.0,
703        n,
704        Some(0.0),
705        Some(1.0),
706        normal_cpoisson(1.0, n, 0.1),
707      ),
708      "Jump: Merton",
709      traj,
710    );
711    grid = grid.register(
712      &Kou::new(
713        0.03,
714        0.2,
715        1.0,
716        0.0,
717        n,
718        Some(0.0),
719        Some(1.0),
720        normal_cpoisson(1.0, n, 0.12),
721      ),
722      "Jump: Kou",
723      traj,
724    );
725    grid = grid.register(
726      &LevyDiffusion::new(
727        0.01,
728        0.2,
729        n,
730        Some(0.0),
731        Some(1.0),
732        normal_cpoisson(1.0, n, 0.08),
733      ),
734      "Jump: Levy Diffusion",
735      traj,
736    );
737    grid = grid.register(
738      &JumpFou::new(
739        0.7,
740        2.0,
741        0.03,
742        0.2,
743        n,
744        Some(0.03),
745        Some(1.0),
746        normal_cpoisson(1.0, n, 0.08),
747      ),
748      "Jump: Jump-Fou",
749      traj,
750    );
751    grid = grid.register(
752      &JumpFOUCustom::new(
753        0.7,
754        2.0,
755        0.03,
756        0.2,
757        n,
758        Some(0.03),
759        Some(1.0),
760        Exp::new(20.0).expect("positive exponential rate"),
761        Exp::new(30.0).expect("positive exponential rate"),
762      ),
763      "Jump: Jump-Fou Custom",
764      traj,
765    );
766    grid = grid.register_with_component_labels(
767      &Bates1996::new(
768        Some(0.03),
769        None,
770        None,
771        None,
772        0.8,
773        0.0,
774        1.5,
775        0.8,
776        0.3,
777        -0.5,
778        n,
779        Some(100.0),
780        Some(0.04),
781        Some(1.0),
782        Some(false),
783        normal_cpoisson(0.8, n, 0.05),
784      ),
785      "Jump: Bates 1996 (S: solid, v: dashed)",
786      &["S", "v"],
787      traj,
788    );
789
790    grid = grid.register(
791      &Heston::new(
792        Some(100.0),
793        Some(0.04),
794        2.0,
795        0.04,
796        0.3,
797        -0.7,
798        0.05,
799        n,
800        Some(1.0),
801        HestonPow::Sqrt,
802        Some(false),
803      ),
804      "Volatility: Heston",
805      traj,
806    );
807    grid = grid.register(
808      &Bergomi::new(0.4, Some(0.2), Some(100.0), 0.01, -0.6, n, Some(1.0)),
809      "Volatility: Bergomi",
810      traj,
811    );
812    grid = grid.register(
813      &RoughBergomi::new(0.1, 0.4, Some(0.2), Some(100.0), 0.01, -0.6, n, Some(1.0)),
814      "Volatility: Rough Bergomi",
815      traj,
816    );
817    grid = grid.register(
818      &RoughHeston::new(0.8, Some(0.2), 0.04, 1.5, 0.3, None, None, Some(1.0), n),
819      "Volatility: Rough Heston",
820      traj,
821    );
822    grid = grid.register(
823      &Sabr::new(0.4, 0.7, -0.3, n, Some(1.0), Some(0.3), Some(1.0)),
824      "Volatility: Sabr",
825      traj,
826    );
827    grid = grid.register(
828      &Svcgmy::new(
829        3.0,
830        4.0,
831        0.7,
832        1.5,
833        0.04,
834        0.3,
835        -0.4,
836        n,
837        j,
838        Some(0.0),
839        Some(0.04),
840        Some(1.0),
841      ),
842      "Volatility: Svcgmy",
843      traj,
844    );
845
846    grid.show();
847
848    let fbs_field = Fbs::new(0.7, sheet_m, sheet_n, 2.0).sample();
849    let z: Vec<Vec<f64>> = fbs_field.outer_iter().map(|row| row.to_vec()).collect();
850    let x: Vec<f64> = (0..sheet_n)
851      .map(|i| i as f64 / (sheet_n.saturating_sub(1).max(1) as f64))
852      .collect();
853    let y: Vec<f64> = (0..sheet_m)
854      .map(|i| i as f64 / (sheet_m.saturating_sub(1).max(1) as f64))
855      .collect();
856
857    let mut sheet_plot = Plot::new();
858    let surface = Surface::new(z).x(x).y(y).name("Sheet: Fbs");
859    sheet_plot.add_trace(surface);
860    sheet_plot.set_layout(
861      Layout::new()
862        .title("Sheet: Fbs (3D Surface)")
863        .height(900)
864        .width(1200),
865    );
866    sheet_plot.show();
867  }
868
869  #[test]
870  fn plot_sde_gbm_all_methods() {
871    use ndarray::Array2;
872    use ndarray::array;
873    use plotly::Layout;
874    use plotly::common::Line;
875    use plotly::layout::Margin;
876    use rand::rng;
877    use stochastic_rs_stochastic::sde::NoiseModel;
878    use stochastic_rs_stochastic::sde::Sde;
879    use stochastic_rs_stochastic::sde::SdeMethod;
880
881    let mu = 0.05;
882    let sigma = 0.2;
883    let x0 = array![100.0];
884    let t0: f64 = 0.0;
885    let t1: f64 = 1.0;
886    let dt: f64 = 0.001;
887    let n_paths = 5;
888    let steps = ((t1 - t0) / dt).ceil() as usize;
889
890    let t_axis: Vec<f64> = (0..=steps).map(|i| t0 + i as f64 * dt).collect();
891
892    let colors = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728"];
893    let methods = [
894      (SdeMethod::Euler, "Euler-Maruyama"),
895      (SdeMethod::Milstein, "Milstein"),
896      (SdeMethod::SRK2, "Midpoint RK2"),
897      (SdeMethod::SRK4, "RK4-style"),
898    ];
899
900    let mut plot = Plot::new();
901    plot.set_layout(
902      Layout::new()
903        .title("Gbm: SDE Solver Methods Comparison (dS = 0.05 S dt + 0.2 S dW)")
904        .auto_size(true)
905        .height(700)
906        .margin(Margin::new().left(60).right(30).top(80).bottom(50)),
907    );
908
909    for (m_idx, (method, method_name)) in methods.into_iter().enumerate() {
910      let sde = Sde::new(
911        move |x: &ndarray::Array1<f64>, _t: f64| array![mu * x[0]],
912        move |x: &ndarray::Array1<f64>, _t: f64| Array2::from_elem((1, 1), sigma * x[0]),
913        NoiseModel::Gaussian,
914        None,
915      );
916
917      let paths = sde.solve(&x0, t0, t1, dt, n_paths, method, &mut rng());
918
919      for p in 0..n_paths {
920        let y: Vec<f64> = (0..=steps).map(|i| paths[[p, i, 0]]).collect();
921        let name = if p == 0 {
922          method_name.to_string()
923        } else {
924          format!("{method_name} (path {p})")
925        };
926        let trace = Scatter::new(t_axis.clone(), y)
927          .mode(Mode::Lines)
928          .line(
929            Line::new()
930              .width(if p == 0 { 2.0 } else { 1.0 })
931              .color(colors[m_idx])
932              .dash(match p {
933                0 => DashType::Solid,
934                1 => DashType::Dash,
935                2 => DashType::Dot,
936                3 => DashType::DashDot,
937                _ => DashType::LongDash,
938              }),
939          )
940          .name(name.as_str())
941          .show_legend(p == 0);
942        plot.add_trace(trace);
943      }
944    }
945
946    let mut path = std::env::temp_dir();
947    path.push("stochastic_rs_sde_gbm_methods.html");
948    plot.write_html(&path);
949    assert!(path.exists(), "expected plot HTML at {}", path.display());
950    let _ = std::fs::remove_file(path);
951  }
952
953  #[test]
954  fn plot_process_writes_html() {
955    let bm = Bm::new(64, Some(1.0));
956    let path_arr = bm.sample();
957    let mut out = std::env::temp_dir();
958    out.push("stochastic_rs_test_plot_process.html");
959    plot_process(&path_arr, out.to_str().unwrap());
960    assert!(out.exists(), "plot_process did not write file");
961    let _ = std::fs::remove_file(out);
962  }
963
964  #[test]
965  fn plot_distribution_writes_html() {
966    let samples: Array1<f64> = (0..256).map(|i| (i as f64) * 0.01).collect();
967    let mut out = std::env::temp_dir();
968    out.push("stochastic_rs_test_plot_distribution.html");
969    plot_distribution(&samples, out.to_str().unwrap(), "test");
970    assert!(out.exists(), "plot_distribution did not write file");
971    let _ = std::fs::remove_file(out);
972  }
973
974  #[test]
975  fn plot_vol_surface_writes_html() {
976    use ndarray::Array2;
977    let strikes = vec![80.0, 90.0, 100.0, 110.0, 120.0];
978    let maturities = vec![0.25, 0.5, 1.0];
979    let ivs = Array2::<f64>::from_shape_fn((maturities.len(), strikes.len()), |(j, i)| {
980      0.2 + 0.01 * (j as f64) - 0.005 * ((i as f64) - 2.0).abs()
981    });
982    let mut out = std::env::temp_dir();
983    out.push("stochastic_rs_test_plot_vol_surface.html");
984    plot_vol_surface(&strikes, &maturities, &ivs, out.to_str().unwrap());
985    assert!(out.exists(), "plot_vol_surface did not write file");
986    let _ = std::fs::remove_file(out);
987  }
988
989  #[test]
990  #[should_panic(expected = "ivs shape must be")]
991  fn plot_vol_surface_rejects_bad_shape() {
992    use ndarray::Array2;
993    let strikes = vec![80.0, 90.0, 100.0];
994    let maturities = vec![0.25, 0.5];
995    let bad = Array2::<f64>::zeros((3, 5));
996    plot_vol_surface(&strikes, &maturities, &bad, "/tmp/should_not_exist.html");
997  }
998
999  #[test]
1000  fn grid_plotter_rescale_threshold_disabled_writes_html() {
1001    let bm = Bm::new(64, Some(1.0));
1002    let grid = GridPlotter::new()
1003      .title("rescale-threshold=None smoke")
1004      .cols(1)
1005      .rescale_threshold(None)
1006      .register(&bm, "Bm", 1);
1007    let plot = grid.plot();
1008    let mut out = std::env::temp_dir();
1009    out.push("stochastic_rs_test_rescale_disabled.html");
1010    plot.write_html(&out);
1011    assert!(out.exists(), "plot did not write file");
1012    let _ = std::fs::remove_file(out);
1013  }
1014}