1use super::control::{ControlProblem, StateDerivatives};
2use super::market_making::MarketMakingControl;
3use super::normal_cdf;
4use crate::numeric::finite_difference::discretization::{DimensionKind, Transport};
5use crate::numeric::finite_difference::pde::PdeProblem;
6
7#[derive(Clone, Debug)]
26pub struct HestonHawkes {
27 pub gamma: f64,
30 pub kappa: f64,
32
33 pub v_kappa: f64,
36 pub v_theta: f64,
38 pub v_xi: f64,
40 pub rho: f64,
43 pub dv: f64,
45
46 pub alpha: f64,
49 pub beta: f64,
51 pub mu_lambda: f64,
53 pub lambda_step: f64,
55 pub dq: f64,
57}
58
59impl HestonHawkes {
60 pub fn new(gamma: f64, kappa: f64) -> Self {
61 Self {
62 gamma,
63 kappa,
64 v_kappa: 2.0,
65 v_theta: 0.25,
66 v_xi: 0.3,
67 rho: 0.0,
68 dv: 1.0,
69 alpha: 0.5,
70 beta: 2.0,
71 mu_lambda: 1.0,
72 lambda_step: 1.0,
73 dq: 1.0,
74 }
75 }
76
77 pub fn with_heston_params(mut self, v_kappa: f64, v_theta: f64, v_xi: f64) -> Self {
78 self.v_kappa = v_kappa;
79 self.v_theta = v_theta;
80 self.v_xi = v_xi;
81 self
82 }
83
84 pub fn with_rho(mut self, rho: f64) -> Self {
85 self.rho = rho;
86 self
87 }
88
89 pub fn with_hawkes_params(mut self, alpha: f64, beta: f64, mu_lambda: f64) -> Self {
90 self.alpha = alpha;
91 self.beta = beta;
92 self.mu_lambda = mu_lambda;
93 self
94 }
95
96 pub fn with_grid_steps(mut self, dx: &[f64; 3]) -> Self {
99 self.dq = dx[0].abs().max(1e-8);
100 self.dv = dx[1].abs().max(1e-8);
101 self.lambda_step = dx[2].abs().max(1e-8);
102 self
103 }
104
105 pub fn get_spreads(&self, derivs: &StateDerivatives<3>) -> (f64, f64) {
109 let base_spread = (1.0 / self.gamma) * (1.0 + self.gamma / self.kappa).ln();
113
114 let dv_dq_buy = derivs.fwd[0];
116 let dv_dq_sell = derivs.bwd[0];
117
118 let mut delta_bid = base_spread - dv_dq_buy;
119 let mut delta_ask = base_spread + dv_dq_sell;
120
121 delta_bid = delta_bid.clamp(-5.0, 10.0);
122 delta_ask = delta_ask.clamp(-5.0, 10.0);
123
124 (delta_bid, delta_ask)
125 }
126
127 pub fn fill_rate_base(&self, state: &[f64; 3]) -> f64 {
130 state[2].max(1e-10)
131 }
132
133 pub fn fill_rate_decay(&self) -> f64 {
135 self.kappa
136 }
137}
138
139impl ControlProblem<3> for HestonHawkes {
140 type Control = MarketMakingControl;
141
142 fn optimize(&self, _t: f64, state: &[f64; 3], derivs: &StateDerivatives<3>) -> Self::Control {
143 let q = state[0];
144 let v = state[1];
145 let lambda = state[2];
146 let base_spread = (1.0 / self.gamma) * (1.0 + self.gamma / self.kappa).ln();
147 let delta_bid = (base_spread - derivs.fwd[0]).clamp(-5.0, 10.0);
148 let delta_ask = (base_spread + derivs.bwd[0]).clamp(-5.0, 10.0);
149
150 let max_fill_mult = 20.0_f64;
151 let lambda_bid = lambda * (-self.kappa * delta_bid).exp().min(max_fill_mult);
152 let lambda_ask = lambda * (-self.kappa * delta_ask).exp().min(max_fill_mult);
153
154 let _ = (q, v);
155 MarketMakingControl::new(lambda_bid, lambda_ask)
156 }
157
158 fn running_reward(&self, _t: f64, _state: &[f64; 3], control: &Self::Control) -> f64 {
159 (control.bid_intensity + control.ask_intensity) / (self.gamma + self.kappa)
160 }
161
162 fn bsde_driver(
163 &self,
164 _t: f64,
165 state: &[f64; 3],
166 control: &Self::Control,
167 _derivs: &StateDerivatives<3>,
168 dt: f64,
169 ) -> f64 {
170 let q = state[0];
176 let v = state[1];
177 let local = -0.5 * self.gamma * v * q.powi(2);
178 let rate_cap = 1.0 / dt.max(1e-12);
179 let reward = (control.bid_intensity.min(rate_cap) + control.ask_intensity.min(rate_cap))
180 / (self.gamma + self.kappa);
181 reward + local
182 }
183
184 fn generator(
185 &self,
186 _t: f64,
187 state: &[f64; 3],
188 _control: &Self::Control,
189 derivs: &StateDerivatives<3>,
190 ) -> f64 {
191 let q = state[0];
192 let v = state[1];
193 let lambda = state[2];
194
195 let v_pos = v.max(1e-5);
196 let mu_v =
197 self.v_kappa * (self.v_theta - v_pos) - self.gamma * self.rho * self.v_xi * v_pos * q;
198 let sigma2_v = self.v_xi.powi(2) * v_pos;
199
200 let net_drift_lambda = self.beta * (self.mu_lambda - lambda) + self.alpha * lambda;
201
202 -0.5 * self.gamma * v * q.powi(2)
203 + mu_v * derivs.grad[1]
204 + 0.5 * sigma2_v * derivs.hessian[1]
205 + net_drift_lambda * derivs.grad[2]
206 }
207
208 fn terminal(&self, _state: &[f64; 3]) -> f64 {
209 0.0
210 }
211
212 fn discount_rate(&self, _state: &[f64; 3]) -> f64 {
213 0.0
214 }
215
216 fn constant_discount_rate(&self) -> Option<f64> {
217 Some(0.0)
218 }
219
220 fn next_step(&self, _t: f64, state: &[f64; 3], dt: f64, noise: &[f64; 3]) -> [f64; 3] {
221 let q = state[0];
222 let v = state[1];
223 let lambda = state[2];
224 let sqrt_dt = dt.sqrt();
225
226 let mut next_q = q;
227 let u = normal_cdf(noise[0]);
228 let base_spread = (1.0 / self.gamma) * (1.0 + self.gamma / self.kappa).ln();
229 let lambda_bid = lambda * (-self.kappa * base_spread).exp();
230 let lambda_ask = lambda * (-self.kappa * base_spread).exp();
231 let p_bid = (lambda_bid * dt).clamp(0.0, 1.0);
232 let p_ask = (lambda_ask * dt).clamp(0.0, 1.0);
233 if u < p_bid {
234 next_q += 1.0;
235 } else if u > 1.0 - p_ask {
236 next_q -= 1.0;
237 }
238
239 let v_pos = v.max(1e-5);
240 let drift_v = self.v_kappa * (self.v_theta - v_pos) * dt;
241 let diff_v = self.v_xi * v_pos.sqrt() * sqrt_dt * noise[1];
242 let mut next_v = v_pos + drift_v + diff_v;
243 if next_v < 0.0 {
244 next_v = -next_v;
245 }
246
247 let net_drift = self.beta * (self.mu_lambda - lambda) + self.alpha * lambda;
248 let next_lambda = (lambda + net_drift * dt).max(0.0);
249
250 [next_q, next_v, next_lambda]
251 }
252
253 fn next_step_controlled(
254 &self,
255 _t: f64,
256 state: &[f64; 3],
257 control: &Self::Control,
258 dt: f64,
259 noise: &[f64; 3],
260 ) -> [f64; 3] {
261 let v = state[1];
262 let lambda = state[2];
263 let sqrt_dt = dt.sqrt();
264
265 let mut next_q = state[0];
266 let u = normal_cdf(noise[0]);
267 let p_bid = (control.bid_intensity * dt).clamp(0.0, 1.0);
268 let p_ask = (control.ask_intensity * dt).clamp(0.0, 1.0);
269 if u < p_bid {
270 next_q += 1.0;
271 } else if u > 1.0 - p_ask {
272 next_q -= 1.0;
273 }
274
275 let v_pos = v.max(1e-5);
276 let drift_v = self.v_kappa * (self.v_theta - v_pos) * dt;
277 let diff_v = self.v_xi * v_pos.sqrt() * sqrt_dt * noise[1];
278 let mut next_v = v_pos + drift_v + diff_v;
279 if next_v < 0.0 {
280 next_v = -next_v;
281 }
282
283 let net_drift = self.beta * (self.mu_lambda - lambda) + self.alpha * lambda;
284 let next_lambda = (lambda + net_drift * dt).max(0.0);
285
286 [next_q, next_v, next_lambda]
287 }
288
289 fn is_diffusion_dimension(&self, dim: usize) -> bool {
290 dim == 1
291 }
292
293 fn gradient_step(&self, dim: usize) -> f64 {
294 match dim {
295 1 => self.dv.abs().max(1e-8),
296 2 => self.lambda_step.abs().max(1e-8),
297 _ => 1.0,
298 }
299 }
300}
301
302impl PdeProblem<3> for HestonHawkes {
303 fn dimension_kind(&self, dim: usize) -> DimensionKind {
304 match dim {
305 0 => DimensionKind::DiscreteJump,
306 1 => DimensionKind::Diffusion,
307 _ => DimensionKind::DeterministicDrift,
308 }
309 }
310
311 fn transport(
312 &self,
313 _t: f64,
314 state: &[f64; 3],
315 control: &Self::Control,
316 derivs: &StateDerivatives<3>,
317 ) -> Transport<3> {
318 let q = state[0];
319 let v = state[1].max(1e-5);
320 let lambda = state[2].max(0.0);
321
322 let mu_v = self.v_kappa * (self.v_theta - v) - self.gamma * self.rho * self.v_xi * v * q;
323 let sigma2_v = self.v_xi.powi(2) * v;
324 let dv = self.dv.abs().max(1e-12);
325
326 let diff_term = sigma2_v / (2.0 * dv * dv);
327 let drift_abs = mu_v.abs() / dv;
328 let (plus_v, minus_v) = if mu_v >= 0.0 {
329 (diff_term + drift_abs, diff_term)
330 } else {
331 (diff_term, diff_term + drift_abs)
332 };
333
334 let net_drift_lambda = self.beta * (self.mu_lambda - lambda) + self.alpha * lambda;
335 let rate = net_drift_lambda.abs() / self.lambda_step.abs().max(1e-12);
336 let (plus_lam, minus_lam) = if net_drift_lambda >= 0.0 {
337 (rate, 0.0)
338 } else {
339 (0.0, rate)
340 };
341
342 let hamiltonian =
343 (control.bid_intensity + control.ask_intensity) / (self.gamma + self.kappa);
344 let jump_transport =
345 control.bid_intensity * derivs.fwd[0] - control.ask_intensity * derivs.bwd[0];
346 let local = -0.5 * self.gamma * v * q.powi(2);
347
348 Transport::new(
349 [control.bid_intensity, plus_v, plus_lam],
350 [control.ask_intensity, minus_v, minus_lam],
351 hamiltonian + local - jump_transport,
352 )
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 fn zero_derivs() -> StateDerivatives<3> {
361 StateDerivatives::new([0.0; 3], [0.0; 3])
362 }
363
364 fn default_model() -> HestonHawkes {
365 HestonHawkes::new(0.5, 1.5)
366 .with_heston_params(2.0, 0.25, 0.3)
367 .with_rho(0.0)
368 .with_hawkes_params(0.5, 2.0, 1.0)
369 .with_grid_steps(&[1.0, 0.02, 0.1])
370 }
371
372 #[test]
373 fn zero_gradient_gives_base_spread() {
374 let m = default_model();
375 let derivs = zero_derivs();
376 let (bid, ask) = m.get_spreads(&derivs);
377 let base = (1.0 / m.gamma) * (1.0 + m.gamma / m.kappa).ln();
378 assert!((bid - base).abs() < 1e-12);
379 assert!((ask - base).abs() < 1e-12);
380 }
381
382 #[test]
383 fn bsde_driver_excludes_variance_and_intensity_transport() {
384 let m = default_model();
385 let state = [2.0, 0.25, 1.0];
386 let control = ControlProblem::optimize(&m, 0.0, &state, &zero_derivs());
387
388 let mut derivs = zero_derivs();
389 derivs.grad[1] = 1.0;
390 derivs.grad[2] = 1.0;
391
392 let dt = 0.01;
393 let q = state[0];
394 let v = state[1];
395 let local = -0.5 * m.gamma * v * q.powi(2);
396 let rate_cap = 1.0 / dt;
397 let reward = (control.bid_intensity.min(rate_cap) + control.ask_intensity.min(rate_cap))
398 / (m.gamma + m.kappa);
399 let expected = reward + local;
400
401 assert_eq!(
402 ControlProblem::bsde_driver(&m, 0.0, &state, &control, &derivs, dt),
403 expected
404 );
405 assert!(
406 ControlProblem::bsde_driver(&m, 0.0, &state, &control, &derivs, dt)
407 != ControlProblem::driver(&m, 0.0, &state, &control, &derivs)
408 );
409 }
410
411 #[test]
412 fn next_step_controlled_uses_optimal_intensities() {
413 let m = default_model();
414 let state = [1.0, 0.25, 1.0];
415
416 let ctrl = MarketMakingControl::new(0.0, 0.0);
417 let next =
418 ControlProblem::next_step_controlled(&m, 0.0, &state, &ctrl, 0.01, &[0.0, 0.0, 0.0]);
419 assert_eq!(next[0], 1.0);
420
421 let ctrl = MarketMakingControl::new(1000.0, 0.0);
422 let next =
423 ControlProblem::next_step_controlled(&m, 0.0, &state, &ctrl, 0.01, &[0.0, 0.0, 0.0]);
424 assert_eq!(next[0], 2.0);
425 }
426
427 #[test]
428 fn rho_affects_variance_drift_in_3d() {
429 let mut derivs = zero_derivs();
430 derivs.grad[1] = 1.0;
431 let state = [2.0, 0.25, 1.0];
432 let control = MarketMakingControl::new(0.0, 0.0);
433
434 let g0 = ControlProblem::generator(
435 &HestonHawkes::new(0.5, 1.5)
436 .with_heston_params(2.0, 0.25, 0.3)
437 .with_rho(0.0)
438 .with_hawkes_params(0.5, 2.0, 1.0)
439 .with_grid_steps(&[1.0, 0.02, 0.1]),
440 0.0,
441 &state,
442 &control,
443 &derivs,
444 );
445
446 let g_pos = ControlProblem::generator(
447 &HestonHawkes::new(0.5, 1.5)
448 .with_heston_params(2.0, 0.25, 0.3)
449 .with_rho(0.5)
450 .with_hawkes_params(0.5, 2.0, 1.0)
451 .with_grid_steps(&[1.0, 0.02, 0.1]),
452 0.0,
453 &state,
454 &control,
455 &derivs,
456 );
457
458 assert!(
459 g0 > g_pos,
460 "Positive rho lowers effective variance drift in the 3D generator"
461 );
462 }
463
464 #[test]
465 fn hawkes_intensity_dimension_uses_upwinding() {
466 let m = default_model();
467 let mut derivs = zero_derivs();
468 derivs.grad[2] = 1.0;
469 let control = MarketMakingControl::new(0.0, 0.0);
470 let g = ControlProblem::generator(&m, 0.0, &[0.0, 0.25, 0.1], &control, &derivs);
471 assert!(g > 0.0);
472 }
473
474 #[test]
475 fn builder_produces_correct_params() {
476 let m = HestonHawkes::new(0.3, 2.0)
477 .with_heston_params(3.0, 0.1, 0.5)
478 .with_rho(-0.2)
479 .with_hawkes_params(0.8, 4.0, 2.0)
480 .with_grid_steps(&[2.0, 0.05, 0.2]);
481 assert_eq!(m.gamma, 0.3);
482 assert_eq!(m.kappa, 2.0);
483 assert_eq!(m.v_kappa, 3.0);
484 assert_eq!(m.rho, -0.2);
485 assert_eq!(m.alpha, 0.8);
486 assert_eq!(m.dq, 2.0);
487 assert_eq!(m.dv, 0.05);
488 assert_eq!(m.lambda_step, 0.2);
489 }
490
491 #[test]
492 fn is_diffusion_dimension_correct() {
493 let m = default_model();
494 assert!(
495 !ControlProblem::is_diffusion_dimension(&m, 0),
496 "Inventory is controlled, not diffusion"
497 );
498 assert!(
499 ControlProblem::is_diffusion_dimension(&m, 1),
500 "Variance is diffusion"
501 );
502 assert!(
503 !ControlProblem::is_diffusion_dimension(&m, 2),
504 "Hawkes intensity is deterministic drift"
505 );
506 }
507
508 #[test]
509 fn dimension_kinds_match_state_space() {
510 let m = default_model();
511 assert_eq!(
512 PdeProblem::dimension_kind(&m, 0),
513 DimensionKind::DiscreteJump
514 );
515 assert_eq!(PdeProblem::dimension_kind(&m, 1), DimensionKind::Diffusion);
516 assert_eq!(
517 PdeProblem::dimension_kind(&m, 2),
518 DimensionKind::DeterministicDrift
519 );
520 }
521}