1use super::traits::{ControlOutput, Gradients, Model};
2
3#[derive(Clone, Debug)]
22pub struct HestonHawkes {
23 pub gamma: f64,
26 pub kappa: f64,
28
29 pub v_kappa: f64,
32 pub v_theta: f64,
34 pub v_xi: f64,
36 pub rho: f64,
39 pub dv: f64,
41
42 pub alpha: f64,
45 pub beta: f64,
47 pub mu_lambda: f64,
49 pub lambda_step: f64,
51 pub dq: f64,
53}
54
55impl HestonHawkes {
56 pub fn new(gamma: f64, kappa: f64) -> Self {
57 Self {
58 gamma,
59 kappa,
60 v_kappa: 2.0,
61 v_theta: 0.25,
62 v_xi: 0.3,
63 rho: 0.0,
64 dv: 1.0,
65 alpha: 0.5,
66 beta: 2.0,
67 mu_lambda: 1.0,
68 lambda_step: 1.0,
69 dq: 1.0,
70 }
71 }
72
73 pub fn with_heston_params(mut self, v_kappa: f64, v_theta: f64, v_xi: f64) -> Self {
74 self.v_kappa = v_kappa;
75 self.v_theta = v_theta;
76 self.v_xi = v_xi;
77 self
78 }
79
80 pub fn with_rho(mut self, rho: f64) -> Self {
81 self.rho = rho;
82 self
83 }
84
85 pub fn with_hawkes_params(mut self, alpha: f64, beta: f64, mu_lambda: f64) -> Self {
86 self.alpha = alpha;
87 self.beta = beta;
88 self.mu_lambda = mu_lambda;
89 self
90 }
91
92 pub fn with_grid_steps(mut self, dx: &[f64; 3]) -> Self {
95 self.dq = dx[0].abs().max(1e-8);
96 self.dv = dx[1].abs().max(1e-8);
97 self.lambda_step = dx[2].abs().max(1e-8);
98 self
99 }
100
101 pub fn get_spreads(&self, _q: f64, v: f64, grads: &Gradients<3>) -> (f64, f64) {
105 let base_spread = (1.0 / self.gamma) * (1.0 + self.gamma / self.kappa).ln();
109
110 let _v_pos = v.max(0.0);
111
112 let dv_dq_buy = grads.fwd[0];
114 let dv_dq_sell = grads.bwd[0];
115
116 let mut delta_bid = base_spread - dv_dq_buy;
117 let mut delta_ask = base_spread + dv_dq_sell;
118
119 delta_bid = delta_bid.clamp(-5.0, 10.0);
120 delta_ask = delta_ask.clamp(-5.0, 10.0);
121
122 (delta_bid, delta_ask)
123 }
124}
125
126impl Model<3> for HestonHawkes {
127 type Process = ();
128
129 fn process(&self) {}
130
131 fn optimize(&self, state: &[f64; 3], grads: &Gradients<3>) -> ControlOutput<3> {
132 let q = state[0];
133 let v = state[1];
134 let lambda = state[2];
135
136 let (d_bid, d_ask) = self.get_spreads(q, v, grads);
138
139 let max_fill_mult = 20.0_f64;
142 let lambda_bid = lambda * (-self.kappa * d_bid).exp().min(max_fill_mult);
143 let lambda_ask = lambda * (-self.kappa * d_ask).exp().min(max_fill_mult);
144
145 let hamiltonian_inv = (lambda_bid + lambda_ask) / (self.gamma + self.kappa);
146 let risk_penalty = -0.5 * self.gamma * v * q.powi(2);
147
148 let drift_correction_q = self.dq * (lambda_bid * grads.fwd[0] - lambda_ask * grads.bwd[0]);
150
151 let v_pos = v.max(1e-5);
155 let mu_v =
156 self.v_kappa * (self.v_theta - v_pos) - self.gamma * self.rho * self.v_xi * v_pos * q;
157 let sigma2_v = self.v_xi.powi(2) * v_pos;
158
159 let diff_term = sigma2_v / (2.0 * self.dv.powi(2));
160 let drift_term_abs = mu_v.abs() / self.dv;
161
162 let mut lambda_v_plus = diff_term;
163 let mut lambda_v_minus = diff_term;
164 if mu_v > 0.0 {
165 lambda_v_plus += drift_term_abs;
166 } else {
167 lambda_v_minus += drift_term_abs;
168 }
169
170 let net_drift_lambda = self.beta * (self.mu_lambda - lambda) + self.alpha * lambda;
172 let drift_abs_lambda = net_drift_lambda.abs() / self.lambda_step;
173
174 let (lambda_lam_plus, lambda_lam_minus) = if net_drift_lambda >= 0.0 {
175 (drift_abs_lambda, 0.0)
176 } else {
177 (0.0, drift_abs_lambda)
178 };
179
180 ControlOutput {
181 lambda_plus: [lambda_bid, lambda_v_plus, lambda_lam_plus],
182 lambda_minus: [lambda_ask, lambda_v_minus, lambda_lam_minus],
183 flow: hamiltonian_inv + risk_penalty - drift_correction_q,
184 }
185 }
186
187 fn terminal(&self, _state: &[f64; 3]) -> f64 {
188 0.0
189 }
190
191 fn constant_discount_rate(&self) -> Option<f64> {
192 Some(0.0)
193 }
194
195 fn next_step(&self, current_state: &[f64; 3], dt: f64, noise: &[f64; 3]) -> [f64; 3] {
196 let q = current_state[0];
197 let v = current_state[1];
198 let lambda = current_state[2];
199 let sqrt_dt = dt.sqrt();
200
201 let next_q = q;
203
204 let v_pos = v.max(1e-5);
206 let drift_v = self.v_kappa * (self.v_theta - v_pos) * dt;
207 let diff_v = self.v_xi * v_pos.sqrt() * sqrt_dt * noise[1];
208 let mut next_v = v_pos + drift_v + diff_v;
209 if next_v < 0.0 {
210 next_v = -next_v;
211 }
212
213 let net_drift = self.beta * (self.mu_lambda - lambda) + self.alpha * lambda;
215 let next_lambda = (lambda + net_drift * dt).max(0.0);
216
217 [next_q, next_v, next_lambda]
218 }
219
220 fn is_diffusion_dimension(&self, dim: usize) -> bool {
221 dim == 1
224 }
225
226 fn is_integer_dimension(&self, dim: usize) -> bool {
227 dim == 0 }
229
230 fn gradient_step(&self, dim: usize) -> f64 {
231 match dim {
232 1 => self.dv.abs().max(1e-8),
233 2 => self.lambda_step.abs().max(1e-8),
234 _ => 1.0,
235 }
236 }
237
238 fn fill_rate_base(&self, state: &[f64; 3]) -> f64 {
241 state[2].max(1e-10)
242 }
243
244 fn fill_rate_decay(&self) -> f64 {
245 self.kappa
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::super::traits::{Gradients, Model};
252 use super::*;
253
254 fn zero_grads() -> Gradients<3> {
255 Gradients {
256 fwd: [0.0; 3],
257 bwd: [0.0; 3],
258 }
259 }
260
261 fn default_model() -> HestonHawkes {
262 HestonHawkes::new(0.5, 1.5)
263 .with_heston_params(2.0, 0.25, 0.3)
264 .with_rho(0.0)
265 .with_hawkes_params(0.5, 2.0, 1.0)
266 .with_grid_steps(&[1.0, 0.02, 0.1])
267 }
268
269 #[test]
270 fn zero_gradient_gives_base_spread() {
271 let m = default_model();
272 let grads = zero_grads();
273 let (bid, ask) = m.get_spreads(0.0, 0.25, &grads);
274 let base = (1.0 / m.gamma) * (1.0 + m.gamma / m.kappa).ln();
275 assert!((bid - base).abs() < 1e-12);
276 assert!((ask - base).abs() < 1e-12);
277 }
278
279 #[test]
280 fn rho_affects_variance_drift_in_3d() {
281 let grads = zero_grads();
282 let state = [2.0, 0.25, 1.0]; let ctrl0 = HestonHawkes::new(0.5, 1.5)
285 .with_heston_params(2.0, 0.25, 0.3)
286 .with_rho(0.0)
287 .with_hawkes_params(0.5, 2.0, 1.0)
288 .with_grid_steps(&[1.0, 0.02, 0.1])
289 .optimize(&state, &grads);
290
291 let ctrl_pos = HestonHawkes::new(0.5, 1.5)
292 .with_heston_params(2.0, 0.25, 0.3)
293 .with_rho(0.5)
294 .with_hawkes_params(0.5, 2.0, 1.0)
295 .with_grid_steps(&[1.0, 0.02, 0.1])
296 .optimize(&state, &grads);
297
298 assert!(
299 ctrl_pos.lambda_minus[1] > ctrl0.lambda_minus[1],
300 "Positive rho should increase downward variance transport in 3D model"
301 );
302 }
303
304 #[test]
305 fn hawkes_intensity_dimension_uses_upwinding() {
306 let m = default_model();
307 let grads = zero_grads();
308 let ctrl = m.optimize(&[0.0, 0.25, 0.1], &grads);
310 assert!(ctrl.lambda_plus[2] > 0.0);
311 assert!(ctrl.lambda_minus[2] < 1e-12);
312 }
313
314 #[test]
315 fn builder_produces_correct_params() {
316 let m = HestonHawkes::new(0.3, 2.0)
317 .with_heston_params(3.0, 0.1, 0.5)
318 .with_rho(-0.2)
319 .with_hawkes_params(0.8, 4.0, 2.0)
320 .with_grid_steps(&[2.0, 0.05, 0.2]);
321 assert_eq!(m.gamma, 0.3);
322 assert_eq!(m.kappa, 2.0);
323 assert_eq!(m.v_kappa, 3.0);
324 assert_eq!(m.rho, -0.2);
325 assert_eq!(m.alpha, 0.8);
326 assert_eq!(m.dq, 2.0);
327 assert_eq!(m.dv, 0.05);
328 assert_eq!(m.lambda_step, 0.2);
329 }
330
331 #[test]
332 fn all_three_dimensions_have_transport() {
333 let m = default_model();
334 let grads = zero_grads();
335 let ctrl = m.optimize(&[1.0, 0.1, 0.5], &grads);
336 assert!(ctrl.lambda_plus[0] > 0.0);
337 assert!(ctrl.lambda_minus[0] > 0.0);
338 assert!(ctrl.lambda_plus[1] > 0.0 || ctrl.lambda_minus[1] > 0.0);
339 assert!(ctrl.lambda_plus[2] > 0.0 || ctrl.lambda_minus[2] > 0.0);
340 }
341
342 #[test]
343 fn is_diffusion_dimension_correct() {
344 let m = default_model();
345 assert!(
346 !m.is_diffusion_dimension(0),
347 "Inventory is controlled, not diffusion"
348 );
349 assert!(m.is_diffusion_dimension(1), "Variance is diffusion");
350 assert!(
351 !m.is_diffusion_dimension(2),
352 "Hawkes intensity is deterministic drift"
353 );
354 }
355}