Skip to main content

market_model/process/
heston.rs

1use crate::process::StochasticProcess;
2use crate::simulatable::Simulatable;
3use ndarray::{Array1, Array2, array};
4use rand::Rng;
5use rand_distr::StandardNormal;
6
7/// Heston stochastic volatility model.
8///
9/// $$dS_t = mu * S_t * dt + sqrt(v_t) * S_t * dW_t^S$$
10/// $$dv_t = kappa * (theta - v_t) * dt + sigma * sqrt(v_t) * dW_t^v$$
11///
12/// with correlation `rho` between the two Brownian motions.
13#[derive(Clone)]
14pub struct HestonProcess {
15    pub mu: f64,
16    pub kappa: f64,
17    pub theta: f64,
18    pub sigma: f64,
19    pub rho: f64,
20    pub price: f64,
21    pub variance: f64,
22    /// Precomputed sqrt(1 - rho^2) for Cholesky.
23    sqrt_one_minus_rho2: f64,
24}
25
26impl HestonProcess {
27    #[allow(clippy::too_many_arguments)]
28    pub fn new(
29        mu: f64,
30        kappa: f64,
31        theta: f64,
32        sigma: f64,
33        rho: f64,
34        initial_price: f64,
35        initial_variance: f64,
36    ) -> Self {
37        let sqrt_one_minus_rho2 = if rho.abs() < 1.0 {
38            (1.0 - rho * rho).sqrt()
39        } else {
40            0.0
41        };
42        Self {
43            mu,
44            kappa,
45            theta,
46            sigma,
47            rho,
48            price: initial_price,
49            variance: initial_variance,
50            sqrt_one_minus_rho2,
51        }
52    }
53
54    pub fn price(&self) -> f64 {
55        self.price
56    }
57
58    pub fn volatility(&self) -> f64 {
59        self.variance.max(0.0).sqrt()
60    }
61
62    /// Closed-form expected variance.
63    pub fn expected_variance(&self, t: f64) -> f64 {
64        let exp = (-self.kappa * t).exp();
65        self.variance * exp + self.theta * (1.0 - exp)
66    }
67
68    /// Closed-form variance of variance.
69    pub fn variance_of_variance(&self, t: f64) -> f64 {
70        let e1 = (-self.kappa * t).exp();
71        let e2 = (-2.0 * self.kappa * t).exp();
72        self.variance * (self.sigma.powi(2) / self.kappa) * (e1 - e2)
73            + (self.theta * self.sigma.powi(2) / (2.0 * self.kappa)) * (1.0 - e1).powi(2)
74    }
75}
76
77impl Simulatable for HestonProcess {
78    type Output = (f64, f64); // (price, variance)
79
80    fn dim(&self) -> usize {
81        2
82    }
83
84    fn step(&mut self, dt: f64, dw: &[f64], _rng: &mut impl Rng) -> (f64, f64) {
85        // Cholesky decomposition of the correlation matrix:
86        // dW^S = dW_1
87        // dW^v = rho * dW_1 + sqrt(1 - rho^2) * dW_2
88        let z1 = dw[0];
89        let z2 = if self.rho.abs() < 1.0 {
90            self.rho * dw[0] + self.sqrt_one_minus_rho2 * dw[1]
91        } else {
92            self.rho.signum() * dw[0]
93        };
94
95        let v = self.variance.max(0.0);
96        let sqrt_v = v.sqrt();
97
98        // Price SDE
99        self.price *= (self.mu * dt + sqrt_v * z1).exp();
100
101        // Variance SDE (full truncation)
102        self.variance += self.kappa * (self.theta - v) * dt + self.sigma * sqrt_v * z2;
103        if self.variance < 0.0 {
104            self.variance = 0.0;
105        }
106
107        (self.price, self.variance)
108    }
109
110    fn current(&self) -> (f64, f64) {
111        (self.price, self.variance)
112    }
113}
114
115impl StochasticProcess for HestonProcess {
116    type State = Array1<f64>;
117    type Diffusion = Array2<f64>;
118    type BrownianIncrement = Array1<f64>;
119
120    fn drift(&self, x: &Array1<f64>, _t: f64) -> Array1<f64> {
121        let s = x[0];
122        let v = x[1];
123        array![self.mu * s, self.kappa * (self.theta - v)]
124    }
125
126    fn diffusion(&self, x: &Array1<f64>, _t: f64) -> Array2<f64> {
127        let s = x[0];
128        let v = if x[1] < 0.0 { 0.0 } else { x[1] };
129        let sqrt_v = v.sqrt();
130        let rho = self.rho;
131        let sqrt_one_minus_rho2 = (1.0 - rho * rho).sqrt();
132
133        array![
134            [sqrt_v * s * sqrt_one_minus_rho2, sqrt_v * s * rho],
135            [0.0, self.sigma * sqrt_v]
136        ]
137    }
138
139    fn jump_size<R: Rng + ?Sized>(&self, x: &Array1<f64>, _t: f64, _rng: &mut R) -> Array1<f64> {
140        Array1::zeros(x.len())
141    }
142
143    fn expected_value(&self, x0: &Array1<f64>, t: f64) -> Array1<f64> {
144        let s0 = x0[0];
145        let v0 = x0[1];
146        let expected_s = s0 * (self.mu * t).exp();
147        let exp_minus_kappa_t = (-self.kappa * t).exp();
148        let expected_v = v0 * exp_minus_kappa_t + self.theta * (1.0 - exp_minus_kappa_t);
149        array![expected_s, expected_v]
150    }
151
152    fn variance(&self, x0: &Array1<f64>, t: f64) -> Array1<f64> {
153        let v0 = x0[1];
154        let var_s = 0.0;
155        let exp_minus_kappa_t = (-self.kappa * t).exp();
156        let exp_minus_2_kappa_t = (-2.0 * self.kappa * t).exp();
157        let var_v =
158            v0 * (self.sigma.powi(2) / self.kappa) * (exp_minus_kappa_t - exp_minus_2_kappa_t)
159                + (self.theta * self.sigma.powi(2) / (2.0 * self.kappa))
160                    * (1.0 - exp_minus_kappa_t).powi(2);
161        array![var_s, var_v]
162    }
163
164    fn step<R: Rng + ?Sized>(
165        &self,
166        x: &Array1<f64>,
167        t: f64,
168        dt: f64,
169        dw: &Array1<f64>,
170        _rng: &mut R,
171    ) -> Array1<f64> {
172        let drift = self.drift(x, t);
173        let diffusion = self.diffusion(x, t);
174        let diffusion_term = diffusion.dot(dw);
175        let mut next_x = x + &(&drift * dt) + &diffusion_term;
176        if next_x[1] < 0.0 {
177            next_x[1] = 0.0;
178        }
179        next_x
180    }
181
182    fn generate_increment<R: Rng + ?Sized>(&self, rng: &mut R, dt: f64) -> Array1<f64> {
183        let dw1: f64 = rng.sample(StandardNormal);
184        let dw2: f64 = rng.sample(StandardNormal);
185        let dw = array![dw1, dw2];
186        dw * dt.sqrt()
187    }
188
189    fn simulate<R: Rng + ?Sized>(
190        &self,
191        x0: Array1<f64>,
192        t0: f64,
193        t_end: f64,
194        dt: f64,
195        rng: &mut R,
196    ) -> Vec<(f64, Array1<f64>)> {
197        let n_steps = ((t_end - t0) / dt).ceil() as usize;
198        let mut path = Vec::with_capacity(n_steps + 1);
199        let mut t = t0;
200        let mut x = x0;
201        path.push((t, x.clone()));
202        let sqrt_dt = dt.sqrt();
203        for _ in 0..n_steps {
204            let dw1: f64 = rng.sample(StandardNormal);
205            let dw2: f64 = rng.sample(StandardNormal);
206            let dw = array![dw1, dw2] * sqrt_dt;
207            x = self.step(&x, t, dt, &dw, rng);
208            t += dt;
209            path.push((t, x.clone()));
210        }
211        path
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn test_heston_dim() {
221        let h = HestonProcess::new(0.05, 2.0, 0.04, 0.3, -0.7, 100.0, 0.04);
222        assert_eq!(h.dim(), 2);
223    }
224
225    #[test]
226    fn test_heston_initial_state() {
227        let h = HestonProcess::new(0.05, 2.0, 0.04, 0.3, -0.7, 100.0, 0.04);
228        let (p, v) = h.current();
229        assert_eq!(p, 100.0);
230        assert_eq!(v, 0.04);
231    }
232
233    #[test]
234    fn test_heston_variance_stays_nonnegative() {
235        let mut h = HestonProcess::new(0.05, 1.0, 0.01, 0.5, -0.7, 100.0, 0.01);
236        for _ in 0..1000 {
237            Simulatable::step(&mut h, 0.001, &[0.0, 0.0], &mut rand::rng());
238            let (_, v) = h.current();
239            assert!(v >= 0.0);
240        }
241    }
242}