Skip to main content

market_model/process/
bates.rs

1use crate::process::StochasticProcess;
2use crate::process::heston::HestonProcess;
3use crate::simulatable::Simulatable;
4use ndarray::{Array1, Array2, array};
5use rand::Rng;
6use rand_distr::{Poisson, StandardNormal};
7
8/// Bates model: Heston stochastic volatility + Merton jump diffusion.
9///
10/// Same as Heston, but with compound Poisson jumps on the price.
11#[derive(Clone)]
12pub struct BatesProcess {
13    heston: HestonProcess,
14    pub lambda: f64,
15    pub mu_jump: f64,
16    pub sigma_jump: f64,
17}
18
19impl BatesProcess {
20    #[allow(clippy::too_many_arguments)]
21    pub fn new(
22        mu: f64,
23        kappa: f64,
24        theta: f64,
25        sigma: f64,
26        rho: f64,
27        lambda: f64,
28        mu_jump: f64,
29        sigma_jump: f64,
30        initial_price: f64,
31        initial_variance: f64,
32    ) -> Self {
33        Self {
34            heston: HestonProcess::new(
35                mu,
36                kappa,
37                theta,
38                sigma,
39                rho,
40                initial_price,
41                initial_variance,
42            ),
43            lambda,
44            mu_jump,
45            sigma_jump,
46        }
47    }
48
49    pub fn price(&self) -> f64 {
50        self.heston.price()
51    }
52
53    pub fn volatility(&self) -> f64 {
54        self.heston.volatility()
55    }
56
57    pub fn mu(&self) -> f64 {
58        self.heston.mu
59    }
60
61    pub fn kappa(&self) -> f64 {
62        self.heston.kappa
63    }
64
65    pub fn theta(&self) -> f64 {
66        self.heston.theta
67    }
68
69    pub fn sigma(&self) -> f64 {
70        self.heston.sigma
71    }
72
73    pub fn rho(&self) -> f64 {
74        self.heston.rho
75    }
76}
77
78impl Simulatable for BatesProcess {
79    type Output = (f64, f64); // (price, variance)
80
81    fn dim(&self) -> usize {
82        2
83    }
84
85    fn step(&mut self, dt: f64, dw: &[f64], rng: &mut impl Rng) -> (f64, f64) {
86        // 1. Heston step (disambiguate via UFCS — both Simulatable and StochasticProcess have step)
87        let (_price, _variance) = Simulatable::step(&mut self.heston, dt, dw, rng);
88
89        // 2. Apply jumps to the price
90        if self.lambda > 0.0 {
91            let p_jump = self.lambda * dt;
92            if rng.random::<f64>() < p_jump {
93                let z: f64 = rng.sample(StandardNormal);
94                let log_jump = self.mu_jump + self.sigma_jump * z;
95                let multiplier = log_jump.exp();
96                self.heston.price *= multiplier;
97            }
98        }
99
100        self.heston.current()
101    }
102
103    fn current(&self) -> (f64, f64) {
104        self.heston.current()
105    }
106}
107
108impl StochasticProcess for BatesProcess {
109    type State = Array1<f64>;
110    type Diffusion = Array2<f64>;
111    type BrownianIncrement = Array1<f64>;
112
113    fn drift(&self, x: &Array1<f64>, t: f64) -> Array1<f64> {
114        self.heston.drift(x, t)
115    }
116
117    fn diffusion(&self, x: &Array1<f64>, t: f64) -> Array2<f64> {
118        self.heston.diffusion(x, t)
119    }
120
121    fn jump_intensity(&self, _x: &Array1<f64>, _t: f64) -> f64 {
122        self.lambda
123    }
124
125    fn jump_size<R: Rng + ?Sized>(&self, x: &Array1<f64>, _t: f64, rng: &mut R) -> Array1<f64> {
126        if self.lambda <= 0.0 {
127            return Array1::zeros(2);
128        }
129        let z: f64 = rng.sample(StandardNormal);
130        let log_jump = self.mu_jump + self.sigma_jump * z;
131        let jump_multiplier = log_jump.exp();
132        let s = x[0];
133        let jump_amount = s * (jump_multiplier - 1.0);
134        array![jump_amount, 0.0]
135    }
136
137    fn expected_value(&self, x0: &Array1<f64>, t: f64) -> Array1<f64> {
138        let s0 = x0[0];
139        let v0 = x0[1];
140        let k = (self.mu_jump + 0.5 * self.sigma_jump.powi(2)).exp() - 1.0;
141        let total_drift = self.heston.mu + self.lambda * k;
142        let expected_s = s0 * (total_drift * t).exp();
143        let exp_minus_kappa_t = (-self.heston.kappa * t).exp();
144        let expected_v = v0 * exp_minus_kappa_t + self.heston.theta * (1.0 - exp_minus_kappa_t);
145        array![expected_s, expected_v]
146    }
147
148    fn variance(&self, x0: &Array1<f64>, t: f64) -> Array1<f64> {
149        self.heston.variance(x0, t)
150    }
151
152    fn step<R: Rng + ?Sized>(
153        &self,
154        x: &Array1<f64>,
155        t: f64,
156        dt: f64,
157        dw: &Array1<f64>,
158        rng: &mut R,
159    ) -> Array1<f64> {
160        let drift = self.drift(x, t);
161        let diffusion = self.diffusion(x, t);
162        let diffusion_term = diffusion.dot(dw);
163        let mut next_x = x + &(&drift * dt) + &diffusion_term;
164        if next_x[1] < 0.0 {
165            next_x[1] = 0.0;
166        }
167        if self.lambda > 0.0 {
168            let poisson = Poisson::new(self.lambda * dt).unwrap();
169            let num_jumps = rng.sample(poisson) as u64;
170            for _ in 0..num_jumps {
171                let jump = self.jump_size(&next_x, t, rng);
172                next_x = next_x + jump;
173            }
174        }
175        next_x
176    }
177
178    fn generate_increment<R: Rng + ?Sized>(&self, rng: &mut R, dt: f64) -> Array1<f64> {
179        self.heston.generate_increment(rng, dt)
180    }
181
182    fn simulate<R: Rng + ?Sized>(
183        &self,
184        x0: Array1<f64>,
185        t0: f64,
186        t_end: f64,
187        dt: f64,
188        rng: &mut R,
189    ) -> Vec<(f64, Array1<f64>)> {
190        let n_steps = ((t_end - t0) / dt).ceil() as usize;
191        let mut path = Vec::with_capacity(n_steps + 1);
192        path.push((t0, x0.clone()));
193        let mut x = x0;
194        let mut t = t0;
195        for _ in 0..n_steps {
196            let dw = self.generate_increment(rng, dt);
197            x = self.step(&x, t, dt, &dw, rng);
198            t += dt;
199            path.push((t, x.clone()));
200        }
201        path
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn test_bates_no_jumps() {
211        let b = BatesProcess::new(0.05, 2.0, 0.04, 0.3, -0.7, 0.0, 0.0, 0.0, 100.0, 0.04);
212        assert_eq!(b.dim(), 2);
213        let (p, v) = b.current();
214        assert_eq!(p, 100.0);
215        assert_eq!(v, 0.04);
216    }
217}