Skip to main content

oxilean_std/stochastic_processes/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::functions::*;
6/// Euler-Maruyama SDE simulator: dX_t = drift(X_t) dt + diffusion(X_t) dW_t.
7///
8/// This can simulate any Itô SDE with time-homogeneous coefficients.
9/// The same `seed` produces the same path for reproducibility.
10pub struct EulerMaruyama {
11    /// Drift coefficient μ(x).
12    pub drift: fn(f64) -> f64,
13    /// Diffusion coefficient σ(x).
14    pub diffusion: fn(f64) -> f64,
15}
16impl EulerMaruyama {
17    /// Create a new Euler-Maruyama integrator with the given coefficients.
18    pub fn new(drift: fn(f64) -> f64, diffusion: fn(f64) -> f64) -> Self {
19        EulerMaruyama { drift, diffusion }
20    }
21    /// Simulate a path starting at `x0` over `[0, t_end]` with `n_steps` steps.
22    ///
23    /// Returns `(time, X_t)` pairs.
24    pub fn simulate(&self, x0: f64, t_end: f64, n_steps: u32, seed: u64) -> Vec<(f64, f64)> {
25        let mut lcg = Lcg::new(seed);
26        let dt = t_end / n_steps as f64;
27        let sqrt_dt = dt.sqrt();
28        let mut path = Vec::with_capacity(n_steps as usize + 1);
29        let mut t = 0.0f64;
30        let mut x = x0;
31        path.push((t, x));
32        for _ in 0..n_steps {
33            let dw = lcg.next_normal() * sqrt_dt;
34            x += (self.drift)(x) * dt + (self.diffusion)(x) * dw;
35            t += dt;
36            path.push((t, x));
37        }
38        path
39    }
40}
41/// Estimates the local time L_t^a of a Brownian motion path at level a.
42///
43/// By the occupation time formula, L_t^a ≈ (1/ε) * ∫₀ᵗ 1_{|B_s - a| < ε} ds.
44#[allow(dead_code)]
45pub struct LocalTimeEstimator {
46    /// The Brownian motion path (time, value) pairs.
47    pub path: Vec<(f64, f64)>,
48}
49impl LocalTimeEstimator {
50    /// Create from a Brownian motion path.
51    pub fn new(path: Vec<(f64, f64)>) -> Self {
52        Self { path }
53    }
54    /// Estimate local time at level `a` using bandwidth `epsilon`.
55    ///
56    /// L_t^a ≈ (1/ε) * Σ_{k} 1_{|B_k - a| < ε} Δt_k.
57    pub fn estimate_local_time(&self, a: f64, epsilon: f64) -> f64 {
58        if self.path.len() < 2 || epsilon <= 0.0 {
59            return 0.0;
60        }
61        let mut total = 0.0f64;
62        for k in 1..self.path.len() {
63            let (t_prev, _) = self.path[k - 1];
64            let (t_curr, x_curr) = self.path[k];
65            let dt = t_curr - t_prev;
66            if (x_curr - a).abs() < epsilon {
67                total += dt;
68            }
69        }
70        total / epsilon
71    }
72    /// Compute the occupation time measure: time spent in interval [lo, hi].
73    pub fn occupation_time(&self, lo: f64, hi: f64) -> f64 {
74        if self.path.len() < 2 {
75            return 0.0;
76        }
77        let mut total = 0.0f64;
78        for k in 1..self.path.len() {
79            let (t_prev, _) = self.path[k - 1];
80            let (t_curr, x_curr) = self.path[k];
81            let dt = t_curr - t_prev;
82            if x_curr >= lo && x_curr < hi {
83                total += dt;
84            }
85        }
86        total
87    }
88    /// Compute the quadratic variation [B]_t ≈ Σ |B_{t_k} - B_{t_{k-1}}|².
89    pub fn quadratic_variation(&self) -> f64 {
90        self.path
91            .windows(2)
92            .map(|w| {
93                let diff = w[1].1 - w[0].1;
94                diff * diff
95            })
96            .sum()
97    }
98}
99/// A continuous-time Markov chain defined by its state space and rate matrix Q.
100///
101/// The rate matrix satisfies: q_{ij} ≥ 0 for i ≠ j, and each row sums to 0
102/// (i.e., q_{ii} = −∑_{j≠i} q_{ij}).
103#[derive(Debug, Clone)]
104pub struct CtMarkovChain {
105    pub states: Vec<String>,
106    /// Q matrix: rate_matrix[i][j] = q_{ij}.
107    pub rate_matrix: Vec<Vec<f64>>,
108}
109impl CtMarkovChain {
110    /// Construct a new CTMC with the given states and rate matrix.
111    pub fn new(states: Vec<String>, rate_matrix: Vec<Vec<f64>>) -> Self {
112        CtMarkovChain {
113            states,
114            rate_matrix,
115        }
116    }
117    /// Approximate the stationary distribution π by power iteration on the
118    /// embedded discrete-time chain (using a uniformisation / balancing heuristic).
119    ///
120    /// Returns `None` if the chain has no states or the matrix is degenerate.
121    pub fn stationary_distribution(&self) -> Option<Vec<f64>> {
122        let n = self.states.len();
123        if n == 0 {
124            return None;
125        }
126        let q_max = self
127            .rate_matrix
128            .iter()
129            .enumerate()
130            .map(|(i, row)| row.get(i).map(|v| v.abs()).unwrap_or(0.0))
131            .fold(0.0f64, f64::max);
132        if q_max < 1e-15 {
133            return Some(vec![1.0 / n as f64; n]);
134        }
135        let mut p = vec![vec![0.0f64; n]; n];
136        for i in 0..n {
137            for j in 0..n {
138                let q_ij = self.rate_matrix[i].get(j).copied().unwrap_or(0.0);
139                p[i][j] = if i == j {
140                    1.0 + q_ij / q_max
141                } else {
142                    q_ij / q_max
143                };
144            }
145        }
146        let mut pi = vec![1.0 / n as f64; n];
147        for _ in 0..10_000 {
148            let mut pi_new = vec![0.0f64; n];
149            for j in 0..n {
150                for i in 0..n {
151                    pi_new[j] += pi[i] * p[i][j];
152                }
153            }
154            let s: f64 = pi_new.iter().sum();
155            if s < 1e-15 {
156                return None;
157            }
158            for v in &mut pi_new {
159                *v /= s;
160            }
161            pi = pi_new;
162        }
163        Some(pi)
164    }
165    /// Crude estimate of the expected hitting time from state `from` to state `to`.
166    ///
167    /// Returns `1 / q_{from,to}` if a direct transition exists, otherwise `f64::INFINITY`.
168    pub fn expected_hitting_time(&self, from: usize, to: usize) -> f64 {
169        if from == to {
170            return 0.0;
171        }
172        let rate = self
173            .rate_matrix
174            .get(from)
175            .and_then(|row| row.get(to))
176            .copied()
177            .unwrap_or(0.0);
178        if rate > 1e-15 {
179            1.0 / rate
180        } else {
181            f64::INFINITY
182        }
183    }
184    /// Crude irreducibility check: returns `true` iff every off-diagonal entry
185    /// of the rate matrix is strictly positive.
186    pub fn is_irreducible(&self) -> bool {
187        let n = self.states.len();
188        for i in 0..n {
189            for j in 0..n {
190                if i == j {
191                    continue;
192                }
193                let q = self
194                    .rate_matrix
195                    .get(i)
196                    .and_then(|r| r.get(j))
197                    .copied()
198                    .unwrap_or(0.0);
199                if q <= 0.0 {
200                    return false;
201                }
202            }
203        }
204        true
205    }
206}
207/// Poisson process simulator.
208///
209/// A Poisson process N_t with rate λ generates random arrival times.
210/// Inter-arrival times are Exponential(λ).
211#[derive(Debug, Clone)]
212pub struct PoissonProcessSimulator {
213    /// Arrival rate λ > 0.
214    pub rate: f64,
215}
216impl PoissonProcessSimulator {
217    /// Create a Poisson process with the given rate λ.
218    pub fn new(rate: f64) -> Self {
219        PoissonProcessSimulator { rate }
220    }
221    /// Simulate arrival times up to time `t_end`.
222    ///
223    /// Returns a vector of arrival times in increasing order.
224    /// Uses the inverse transform method for Exponential inter-arrivals.
225    pub fn arrival_times(&self, t_end: f64, seed: u64) -> Vec<f64> {
226        let mut lcg = Lcg::new(seed);
227        let mut times = Vec::new();
228        let mut t = 0.0f64;
229        loop {
230            let u = lcg.next_f64().max(1e-15);
231            t += (-u.ln()) / self.rate;
232            if t > t_end {
233                break;
234            }
235            times.push(t);
236        }
237        times
238    }
239    /// Simulate the counting process N_t as `(time, count)` pairs.
240    ///
241    /// Returns a path with a jump of +1 at each arrival time, sampled at `n_steps`
242    /// equally-spaced time points in `[0, t_end]`.
243    pub fn counting_process(&self, t_end: f64, n_steps: u32, seed: u64) -> Vec<(f64, u64)> {
244        let arrivals = self.arrival_times(t_end, seed);
245        let dt = t_end / n_steps as f64;
246        let mut path = Vec::with_capacity(n_steps as usize + 1);
247        let mut count = 0u64;
248        let mut arrival_idx = 0usize;
249        for k in 0..=n_steps {
250            let t = k as f64 * dt;
251            while arrival_idx < arrivals.len() && arrivals[arrival_idx] <= t {
252                count += 1;
253                arrival_idx += 1;
254            }
255            path.push((t, count));
256        }
257        path
258    }
259    /// Expected number of arrivals in [0, t]: E[N_t] = λt.
260    pub fn expected_count(&self, t: f64) -> f64 {
261        self.rate * t
262    }
263}
264/// Compound Poisson process Y_t = Σ_{i=1}^{N_t} Z_i.
265///
266/// N_t is a Poisson process with rate λ, and the jump sizes Z_i are i.i.d.
267/// Here we use standard normal jump sizes for illustration.
268#[derive(Debug, Clone)]
269pub struct CompoundPoissonProcess {
270    /// Arrival rate λ > 0.
271    pub rate: f64,
272    /// Mean of each jump.
273    pub jump_mean: f64,
274    /// Standard deviation of each jump.
275    pub jump_std: f64,
276}
277impl CompoundPoissonProcess {
278    /// Create a compound Poisson process with Gaussian jumps.
279    pub fn new(rate: f64, jump_mean: f64, jump_std: f64) -> Self {
280        CompoundPoissonProcess {
281            rate,
282            jump_mean,
283            jump_std,
284        }
285    }
286    /// Simulate a path of Y_t up to `t_end`, sampled at `n_steps` equally-spaced points.
287    ///
288    /// Returns `(time, Y_t)` pairs.
289    pub fn simulate(&self, t_end: f64, n_steps: u32, seed: u64) -> Vec<(f64, f64)> {
290        let mut lcg = Lcg::new(seed);
291        let poisson = PoissonProcessSimulator::new(self.rate);
292        let arrivals = poisson.arrival_times(t_end, seed.wrapping_add(99_999));
293        let jumps: Vec<f64> = arrivals
294            .iter()
295            .map(|_| self.jump_mean + self.jump_std * lcg.next_normal())
296            .collect();
297        let dt = t_end / n_steps as f64;
298        let mut path = Vec::with_capacity(n_steps as usize + 1);
299        let mut cumulative = 0.0f64;
300        let mut arrival_idx = 0usize;
301        for k in 0..=n_steps {
302            let t = k as f64 * dt;
303            while arrival_idx < arrivals.len() && arrivals[arrival_idx] <= t {
304                cumulative += jumps[arrival_idx];
305                arrival_idx += 1;
306            }
307            path.push((t, cumulative));
308        }
309        path
310    }
311    /// Theoretical mean of Y_t: E[Y_t] = λ t E[Z].
312    pub fn expected_value(&self, t: f64) -> f64 {
313        self.rate * t * self.jump_mean
314    }
315    /// Theoretical variance of Y_t: Var[Y_t] = λ t E[Z²] = λ t (σ² + μ²).
316    pub fn variance(&self, t: f64) -> f64 {
317        self.rate * t * (self.jump_std * self.jump_std + self.jump_mean * self.jump_mean)
318    }
319}
320/// Milstein scheme for SDEs: dX_t = μ(X_t) dt + σ(X_t) dW_t.
321///
322/// The Milstein correction adds a term ½ σ σ' ((ΔW)² - Δt) to achieve
323/// strong order 1.0 convergence (vs. Euler-Maruyama's order 0.5).
324#[allow(dead_code)]
325pub struct MilsteinScheme {
326    /// Drift coefficient μ(x).
327    pub drift: fn(f64) -> f64,
328    /// Diffusion coefficient σ(x).
329    pub diffusion: fn(f64) -> f64,
330    /// Derivative of diffusion σ'(x).
331    pub diffusion_deriv: fn(f64) -> f64,
332}
333impl MilsteinScheme {
334    /// Create a new Milstein integrator.
335    pub fn new(
336        drift: fn(f64) -> f64,
337        diffusion: fn(f64) -> f64,
338        diffusion_deriv: fn(f64) -> f64,
339    ) -> Self {
340        MilsteinScheme {
341            drift,
342            diffusion,
343            diffusion_deriv,
344        }
345    }
346    /// Simulate a path starting at `x0` over `[0, t_end]` with `n_steps` steps.
347    ///
348    /// Returns `(time, X_t)` pairs.
349    pub fn simulate(&self, x0: f64, t_end: f64, n_steps: u32, seed: u64) -> Vec<(f64, f64)> {
350        let mut lcg = Lcg::new(seed);
351        let dt = t_end / n_steps as f64;
352        let sqrt_dt = dt.sqrt();
353        let mut path = Vec::with_capacity(n_steps as usize + 1);
354        let mut t = 0.0f64;
355        let mut x = x0;
356        path.push((t, x));
357        for _ in 0..n_steps {
358            let dw = lcg.next_normal() * sqrt_dt;
359            let sig = (self.diffusion)(x);
360            let sig_p = (self.diffusion_deriv)(x);
361            x += (self.drift)(x) * dt + sig * dw + 0.5 * sig * sig_p * (dw * dw - dt);
362            t += dt;
363            path.push((t, x));
364        }
365        path
366    }
367    /// Estimate the mean E[X_T] using Monte Carlo with `n_paths` paths.
368    pub fn monte_carlo_mean(
369        &self,
370        x0: f64,
371        t_end: f64,
372        n_steps: u32,
373        n_paths: u32,
374        seed: u64,
375    ) -> f64 {
376        if n_paths == 0 {
377            return 0.0;
378        }
379        let total: f64 = (0..n_paths)
380            .map(|i| {
381                let path = self.simulate(x0, t_end, n_steps, seed.wrapping_add(i as u64));
382                path.last().map(|&(_, x)| x).unwrap_or(x0)
383            })
384            .sum();
385        total / n_paths as f64
386    }
387}
388/// The Heston stochastic volatility model:
389///   dS = μ S dt + √V S dW₁
390///   dV = κ(θ - V) dt + ξ √V dW₂
391/// with correlation corr(dW₁, dW₂) = ρ.
392///
393/// Uses Euler-Maruyama discretization.
394#[allow(dead_code)]
395pub struct HestonModel {
396    /// Drift μ of the asset.
397    pub mu: f64,
398    /// Mean reversion speed κ > 0.
399    pub kappa: f64,
400    /// Long-run variance θ > 0.
401    pub theta: f64,
402    /// Vol-of-vol ξ > 0.
403    pub xi: f64,
404    /// Correlation ρ ∈ (-1, 1).
405    pub rho: f64,
406}
407impl HestonModel {
408    /// Create a new Heston model.
409    pub fn new(mu: f64, kappa: f64, theta: f64, xi: f64, rho: f64) -> Self {
410        HestonModel {
411            mu,
412            kappa,
413            theta,
414            xi,
415            rho,
416        }
417    }
418    /// Simulate (S_t, V_t) paths using Euler-Maruyama.
419    ///
420    /// Returns Vec<(time, S_t, V_t)>.
421    pub fn simulate(
422        &self,
423        s0: f64,
424        v0: f64,
425        t_end: f64,
426        n_steps: u32,
427        seed: u64,
428    ) -> Vec<(f64, f64, f64)> {
429        let mut lcg = Lcg::new(seed);
430        let dt = t_end / n_steps as f64;
431        let sqrt_dt = dt.sqrt();
432        let mut path = Vec::with_capacity(n_steps as usize + 1);
433        let mut t = 0.0f64;
434        let mut s = s0;
435        let mut v = v0.max(0.0);
436        path.push((t, s, v));
437        for _ in 0..n_steps {
438            let z1 = lcg.next_normal();
439            let z2 = lcg.next_normal();
440            let w1 = z1;
441            let w2 = self.rho * z1 + (1.0 - self.rho * self.rho).max(0.0).sqrt() * z2;
442            let sqrt_v = v.max(0.0).sqrt();
443            s *= 1.0 + self.mu * dt + sqrt_v * w1 * sqrt_dt;
444            v = (v + self.kappa * (self.theta - v) * dt + self.xi * sqrt_v * w2 * sqrt_dt).max(0.0);
445            t += dt;
446            path.push((t, s, v));
447        }
448        path
449    }
450    /// Feller condition check: 2κθ > ξ² ensures V never hits 0.
451    pub fn feller_condition_satisfied(&self) -> bool {
452        2.0 * self.kappa * self.theta > self.xi * self.xi
453    }
454    /// Long-run mean of the variance: θ.
455    pub fn variance_long_run_mean(&self) -> f64 {
456        self.theta
457    }
458}
459/// Geometric Brownian Motion simulator for asset price modelling.
460///
461/// Models: dS = μ S dt + σ S dW.
462/// The exact solution is S_t = S_0 exp((μ - σ²/2)t + σW_t).
463#[derive(Debug, Clone)]
464pub struct GeometricBrownianMotionProcess {
465    /// Drift (expected return) μ.
466    pub mu: f64,
467    /// Volatility σ ≥ 0.
468    pub sigma: f64,
469}
470impl GeometricBrownianMotionProcess {
471    /// Create a GBM with given drift and volatility.
472    pub fn new(mu: f64, sigma: f64) -> Self {
473        GeometricBrownianMotionProcess { mu, sigma }
474    }
475    /// Simulate a path starting at `s0` over `[0, t_end]` with `n_steps` steps.
476    ///
477    /// Returns `(time, S_t)` pairs.
478    pub fn simulate(&self, s0: f64, t_end: f64, n_steps: u32, seed: u64) -> Vec<(f64, f64)> {
479        geometric_brownian_motion(s0, self.mu, self.sigma, t_end, n_steps, seed)
480    }
481    /// Expected value E[S_t] = S_0 exp(μ t).
482    pub fn expected_value(&self, s0: f64, t: f64) -> f64 {
483        s0 * (self.mu * t).exp()
484    }
485    /// Variance Var[S_t] = S_0² e^{2μt} (e^{σ²t} - 1).
486    pub fn variance(&self, s0: f64, t: f64) -> f64 {
487        let s0sq = s0 * s0;
488        s0sq * (2.0 * self.mu * t).exp() * ((self.sigma * self.sigma * t).exp() - 1.0)
489    }
490}
491/// Black-Scholes option pricer for European options.
492///
493/// Prices calls and puts using the closed-form Black-Scholes formula and
494/// also computes the standard Greeks (delta, gamma, vega, theta, rho).
495#[derive(Debug, Clone)]
496pub struct BlackScholesPricer {
497    /// Current asset price S.
498    pub spot: f64,
499    /// Strike price K.
500    pub strike: f64,
501    /// Time to expiry T (in years).
502    pub time_to_expiry: f64,
503    /// Risk-free interest rate r.
504    pub rate: f64,
505    /// Implied volatility σ.
506    pub volatility: f64,
507}
508impl BlackScholesPricer {
509    /// Create a new pricer with the given market parameters.
510    pub fn new(spot: f64, strike: f64, time_to_expiry: f64, rate: f64, volatility: f64) -> Self {
511        BlackScholesPricer {
512            spot,
513            strike,
514            time_to_expiry,
515            rate,
516            volatility,
517        }
518    }
519    fn d1(&self) -> f64 {
520        let sqrt_t = self.time_to_expiry.sqrt();
521        ((self.spot / self.strike).ln()
522            + (self.rate + 0.5 * self.volatility * self.volatility) * self.time_to_expiry)
523            / (self.volatility * sqrt_t)
524    }
525    fn d2(&self) -> f64 {
526        self.d1() - self.volatility * self.time_to_expiry.sqrt()
527    }
528    /// European call option price.
529    pub fn call_price(&self) -> f64 {
530        black_scholes_call(
531            self.spot,
532            self.strike,
533            self.time_to_expiry,
534            self.rate,
535            self.volatility,
536        )
537    }
538    /// European put option price.
539    pub fn put_price(&self) -> f64 {
540        black_scholes_put(
541            self.spot,
542            self.strike,
543            self.time_to_expiry,
544            self.rate,
545            self.volatility,
546        )
547    }
548    /// Delta for a call: ∂C/∂S = N(d₁).
549    pub fn call_delta(&self) -> f64 {
550        if self.time_to_expiry <= 0.0 {
551            return if self.spot > self.strike { 1.0 } else { 0.0 };
552        }
553        standard_normal_cdf(self.d1())
554    }
555    /// Delta for a put: ∂P/∂S = N(d₁) − 1.
556    pub fn put_delta(&self) -> f64 {
557        self.call_delta() - 1.0
558    }
559    /// Gamma (same for call and put): ∂²C/∂S² = φ(d₁) / (S σ √T).
560    pub fn gamma(&self) -> f64 {
561        if self.time_to_expiry <= 0.0 {
562            return 0.0;
563        }
564        let d1 = self.d1();
565        let phi = (-0.5 * d1 * d1).exp() / (2.0 * std::f64::consts::PI).sqrt();
566        phi / (self.spot * self.volatility * self.time_to_expiry.sqrt())
567    }
568    /// Vega (same for call and put): ∂C/∂σ = S φ(d₁) √T.
569    pub fn vega(&self) -> f64 {
570        if self.time_to_expiry <= 0.0 {
571            return 0.0;
572        }
573        let d1 = self.d1();
574        let phi = (-0.5 * d1 * d1).exp() / (2.0 * std::f64::consts::PI).sqrt();
575        self.spot * phi * self.time_to_expiry.sqrt()
576    }
577    /// Rho for a call: ∂C/∂r = K T e^{-rT} N(d₂).
578    pub fn call_rho(&self) -> f64 {
579        if self.time_to_expiry <= 0.0 {
580            return 0.0;
581        }
582        self.strike
583            * self.time_to_expiry
584            * (-self.rate * self.time_to_expiry).exp()
585            * standard_normal_cdf(self.d2())
586    }
587    /// Rho for a put: ∂P/∂r = -K T e^{-rT} N(-d₂).
588    pub fn put_rho(&self) -> f64 {
589        if self.time_to_expiry <= 0.0 {
590            return 0.0;
591        }
592        -self.strike
593            * self.time_to_expiry
594            * (-self.rate * self.time_to_expiry).exp()
595            * standard_normal_cdf(-self.d2())
596    }
597    /// Implied volatility via bisection (Newton-Raphson seed).
598    ///
599    /// Given an observed call price, find σ such that BS(σ) = market_price.
600    /// Returns `None` if the market price is outside the no-arbitrage bounds.
601    pub fn implied_volatility_call(&self, market_price: f64) -> Option<f64> {
602        let intrinsic =
603            (self.spot - self.strike * (-self.rate * self.time_to_expiry).exp()).max(0.0);
604        if market_price < intrinsic {
605            return None;
606        }
607        let mut lo = 1e-6f64;
608        let mut hi = 10.0f64;
609        for _ in 0..200 {
610            let mid = 0.5 * (lo + hi);
611            let pricer = BlackScholesPricer {
612                volatility: mid,
613                ..*self
614            };
615            let price = pricer.call_price();
616            if (price - market_price).abs() < 1e-8 {
617                return Some(mid);
618            }
619            if price < market_price {
620                lo = mid;
621            } else {
622                hi = mid;
623            }
624        }
625        Some(0.5 * (lo + hi))
626    }
627}
628/// The Variance-Gamma process: X_t = μ G_t + σ W_{G_t}
629/// where G_t is a Gamma process (subordinator) and W is a Brownian motion.
630///
631/// VG is a popular model in mathematical finance (Madan-Seneta model).
632#[allow(dead_code)]
633pub struct VarianceGammaProcess {
634    /// Drift parameter μ (asymmetry).
635    pub mu: f64,
636    /// Volatility parameter σ.
637    pub sigma: f64,
638    /// Variance rate ν of the Gamma subordinator.
639    pub nu: f64,
640}
641impl VarianceGammaProcess {
642    /// Create a VG process with parameters (μ, σ, ν).
643    pub fn new(mu: f64, sigma: f64, nu: f64) -> Self {
644        VarianceGammaProcess { mu, sigma, nu }
645    }
646    /// Simulate a VG path starting at `x0` over `[0, t_end]` with `n_steps` steps.
647    ///
648    /// At each step, sample dG ~ Gamma(dt/ν, 1/ν), then X += μ dG + σ √(dG) Z.
649    pub fn simulate(&self, x0: f64, t_end: f64, n_steps: u32, seed: u64) -> Vec<(f64, f64)> {
650        let mut lcg = Lcg::new(seed);
651        let dt = t_end / n_steps as f64;
652        let mut path = Vec::with_capacity(n_steps as usize + 1);
653        let mut t = 0.0f64;
654        let mut x = x0;
655        path.push((t, x));
656        for _ in 0..n_steps {
657            let dg = sp_ext_gamma_sample(dt / self.nu, 1.0 / self.nu, &mut lcg);
658            let z = lcg.next_normal();
659            x += self.mu * dg + self.sigma * dg.sqrt() * z;
660            t += dt;
661            path.push((t, x));
662        }
663        path
664    }
665    /// Theoretical mean of X_t: E[X_t] = x0 + μ t.
666    pub fn theoretical_mean(&self, x0: f64, t: f64) -> f64 {
667        x0 + self.mu * t
668    }
669    /// Theoretical variance of X_t: Var[X_t] = σ² t + μ² ν t.
670    pub fn theoretical_variance(&self, t: f64) -> f64 {
671        self.sigma * self.sigma * t + self.mu * self.mu * self.nu * t
672    }
673    /// The VG characteristic exponent ψ(u) = log E[e^{iuX_1}].
674    pub fn characteristic_exponent(&self, u: f64) -> f64 {
675        let a = 1.0 - self.mu * self.nu * u * u + 0.5 * self.sigma * self.sigma * self.nu * u * u;
676        let b = self.mu * self.nu * u;
677        let modulus_sq = a * a + b * b;
678        if modulus_sq <= 0.0 {
679            return 0.0;
680        }
681        -0.5 * modulus_sq.ln() / self.nu
682    }
683}
684/// Ornstein-Uhlenbeck process simulator.
685///
686/// Models mean-reverting dynamics: dX = θ(μ - X) dt + σ dW.
687/// Useful for interest rate models (Vasicek) and volatility models.
688#[derive(Debug, Clone)]
689pub struct OrnsteinUhlenbeckProcess {
690    /// Mean reversion speed θ > 0.
691    pub theta: f64,
692    /// Long-run mean μ.
693    pub mean: f64,
694    /// Volatility σ ≥ 0.
695    pub sigma: f64,
696}
697impl OrnsteinUhlenbeckProcess {
698    /// Create an OU process with given parameters.
699    pub fn new(theta: f64, mean: f64, sigma: f64) -> Self {
700        OrnsteinUhlenbeckProcess { theta, mean, sigma }
701    }
702    /// Simulate a path starting at `x0` over `[0, t_end]` with `n_steps` steps.
703    ///
704    /// Returns `(time, X_t)` pairs using exact conditional update.
705    pub fn simulate(&self, x0: f64, t_end: f64, n_steps: u32, seed: u64) -> Vec<(f64, f64)> {
706        ornstein_uhlenbeck(x0, self.theta, self.mean, self.sigma, t_end, n_steps, seed)
707    }
708    /// Long-run variance: σ² / (2θ).
709    pub fn stationary_variance(&self) -> f64 {
710        self.sigma * self.sigma / (2.0 * self.theta)
711    }
712    /// Long-run standard deviation: σ / √(2θ).
713    pub fn stationary_std(&self) -> f64 {
714        self.stationary_variance().sqrt()
715    }
716}
717/// A subordinated process: X_{T_t} where T is a subordinator.
718///
719/// A subordinator is a non-decreasing Lévy process. We model it via a
720/// gamma process approximation (increments ~ Gamma(a*dt, b)).
721#[allow(dead_code)]
722pub struct SubordinatedProcess {
723    /// The base process drift.
724    pub base_drift: f64,
725    /// The base process volatility.
726    pub base_sigma: f64,
727    /// Gamma process parameter a (shape rate).
728    pub gamma_a: f64,
729    /// Gamma process parameter b (scale).
730    pub gamma_b: f64,
731}
732impl SubordinatedProcess {
733    /// Create a new subordinated process.
734    pub fn new(base_drift: f64, base_sigma: f64, gamma_a: f64, gamma_b: f64) -> Self {
735        SubordinatedProcess {
736            base_drift,
737            base_sigma,
738            gamma_a,
739            gamma_b,
740        }
741    }
742    /// Simulate a path of the subordinated process over `[0, t_end]` with `n_steps` steps.
743    ///
744    /// Returns `(time, X_{T_t})` pairs.
745    pub fn simulate(&self, x0: f64, t_end: f64, n_steps: u32, seed: u64) -> Vec<(f64, f64)> {
746        let mut lcg = Lcg::new(seed);
747        let dt = t_end / n_steps as f64;
748        let mut path = Vec::with_capacity(n_steps as usize + 1);
749        let mut t = 0.0f64;
750        let mut x = x0;
751        path.push((t, x));
752        for _ in 0..n_steps {
753            let d_sub = sp_ext_gamma_sample(self.gamma_a * dt, self.gamma_b, &mut lcg);
754            let dw = lcg.next_normal() * d_sub.sqrt();
755            x += self.base_drift * d_sub + self.base_sigma * dw;
756            t += dt;
757            path.push((t, x));
758        }
759        path
760    }
761    /// Theoretical mean E[X_t] = x0 + base_drift * gamma_a/gamma_b * t.
762    pub fn theoretical_mean(&self, x0: f64, t: f64) -> f64 {
763        x0 + self.base_drift * (self.gamma_a / self.gamma_b) * t
764    }
765    /// Theoretical variance Var[X_t] for subordinated Brownian motion.
766    pub fn theoretical_variance(&self, t: f64) -> f64 {
767        let e_sub = self.gamma_a / self.gamma_b * t;
768        let var_sub = self.gamma_a / (self.gamma_b * self.gamma_b) * t;
769        self.base_sigma * self.base_sigma * e_sub + self.base_drift * self.base_drift * var_sub
770    }
771}
772/// Linear congruential generator (Knuth / glibc parameters).
773/// Returns values in [0, 2^32).
774pub(super) struct Lcg {
775    state: u64,
776}
777impl Lcg {
778    pub(super) fn new(seed: u64) -> Self {
779        Lcg {
780            state: seed.wrapping_add(1),
781        }
782    }
783    /// Advance one step and return the next u32 value.
784    pub(super) fn next_u32(&mut self) -> u32 {
785        self.state = self
786            .state
787            .wrapping_mul(6364136223846793005)
788            .wrapping_add(1442695040888963407);
789        (self.state >> 33) as u32
790    }
791    /// Return a uniform float in [0, 1).
792    pub(super) fn next_f64(&mut self) -> f64 {
793        self.next_u32() as f64 / (u32::MAX as f64 + 1.0)
794    }
795    /// Return ±1 with equal probability.
796    pub(super) fn next_step(&mut self) -> f64 {
797        if self.next_u32() & 1 == 0 {
798            1.0
799        } else {
800            -1.0
801        }
802    }
803    /// Box-Muller transform: return a standard normal sample.
804    pub(super) fn next_normal(&mut self) -> f64 {
805        let u1 = self.next_f64().max(1e-15);
806        let u2 = self.next_f64();
807        (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
808    }
809}