1use super::TerminalCondition;
2use super::control::{ControlProblem, StateDerivatives};
3use super::market_making::MarketMakingControl;
4use super::normal_cdf;
5use crate::numeric::finite_difference::discretization::{DimensionKind, Transport};
6use crate::numeric::finite_difference::pde::PdeProblem;
7
8#[derive(Clone)]
24pub struct AvellanedaHawkes {
25 pub gamma: f64,
26 pub sigma: f64,
27 pub kappa: f64,
28 pub alpha: f64, pub beta: f64, pub mu: f64, pub lambda_step: f64,
33 pub dq: f64,
35 pub terminal_condition: TerminalCondition,
37 pub q_min: f64,
39 pub q_max: f64,
41 pub terminal_liquidation_half_spread: Option<f64>,
44}
45
46impl AvellanedaHawkes {
47 pub fn new(gamma: f64, sigma: f64, kappa: f64, alpha: f64, beta: f64, mu: f64) -> Self {
48 Self {
49 gamma,
50 sigma,
51 kappa,
52 alpha,
53 beta,
54 mu,
55 lambda_step: 1.0,
56 dq: 1.0,
57 terminal_condition: TerminalCondition::Zero,
58 q_min: f64::NEG_INFINITY,
59 q_max: f64::INFINITY,
60 terminal_liquidation_half_spread: None,
61 }
62 }
63
64 pub fn with_terminal_liquidation_half_spread(mut self, half_spread: f64) -> Self {
65 self.terminal_liquidation_half_spread = Some(half_spread.max(0.0));
66 self
67 }
68
69 pub fn with_inventory_bounds(mut self, q_min: f64, q_max: f64) -> Self {
70 self.q_min = q_min;
71 self.q_max = q_max;
72 self
73 }
74
75 pub fn with_terminal_condition(mut self, terminal_condition: TerminalCondition) -> Self {
76 self.terminal_condition = terminal_condition;
77 self
78 }
79
80 pub fn with_lambda_step(mut self, lambda_step: f64) -> Self {
81 self.lambda_step = lambda_step.abs().max(1e-8);
82 self
83 }
84
85 pub fn with_dq(mut self, dq: f64) -> Self {
86 self.dq = dq.abs().max(1e-8);
87 self
88 }
89
90 pub fn get_spreads(&self, _lambda: f64, derivs: &StateDerivatives<2>) -> (f64, f64) {
93 let base_spread = (1.0 / self.gamma) * (1.0 + self.gamma / self.kappa).ln();
94
95 let dv_dq_buy = derivs.fwd[0];
96 let dv_dq_sell = derivs.bwd[0];
97
98 let mut delta_bid = base_spread - dv_dq_buy;
99 let mut delta_ask = base_spread + dv_dq_sell;
100
101 let min_spread = -10.0;
102 let max_spread = 10.0;
103 delta_bid = delta_bid.max(min_spread).min(max_spread);
104 delta_ask = delta_ask.max(min_spread).min(max_spread);
105
106 (delta_bid, delta_ask)
107 }
108
109 pub fn fill_rate_base(&self, state: &[f64; 2]) -> f64 {
112 state[1].max(1e-10)
113 }
114
115 pub fn fill_rate_decay(&self) -> f64 {
117 self.kappa
118 }
119}
120
121impl ControlProblem<2> for AvellanedaHawkes {
122 type Control = MarketMakingControl;
123
124 fn optimize(&self, _t: f64, state: &[f64; 2], derivs: &StateDerivatives<2>) -> Self::Control {
125 let q = state[0];
126 let lambda = state[1];
127 let base_spread = (1.0 / self.gamma) * (1.0 + self.gamma / self.kappa).ln();
128 let delta_bid = (base_spread - derivs.fwd[0]).clamp(-10.0, 10.0);
129 let delta_ask = (base_spread + derivs.bwd[0]).clamp(-10.0, 10.0);
130
131 let max_fill_mult = 20.0_f64;
132 let lambda_bid = lambda * (-self.kappa * delta_bid).exp().min(max_fill_mult);
133 let lambda_ask = lambda * (-self.kappa * delta_ask).exp().min(max_fill_mult);
134
135 let lambda_bid = if q >= self.q_max { 0.0 } else { lambda_bid };
136 let lambda_ask = if q <= self.q_min { 0.0 } else { lambda_ask };
137
138 MarketMakingControl::new(lambda_bid, lambda_ask)
139 }
140
141 fn running_reward(&self, _t: f64, _state: &[f64; 2], control: &Self::Control) -> f64 {
142 (control.bid_intensity + control.ask_intensity) / (self.gamma + self.kappa)
143 }
144
145 fn bsde_driver(
146 &self,
147 _t: f64,
148 state: &[f64; 2],
149 control: &Self::Control,
150 _derivs: &StateDerivatives<2>,
151 dt: f64,
152 ) -> f64 {
153 let q = state[0];
159 let local = -0.5 * self.gamma * self.sigma.powi(2) * q.powi(2);
160 let rate_cap = 1.0 / dt.max(1e-12);
161 let reward = (control.bid_intensity.min(rate_cap) + control.ask_intensity.min(rate_cap))
162 / (self.gamma + self.kappa);
163 reward + local
164 }
165
166 fn generator(
167 &self,
168 _t: f64,
169 state: &[f64; 2],
170 _control: &Self::Control,
171 derivs: &StateDerivatives<2>,
172 ) -> f64 {
173 let q = state[0];
174 let lambda = state[1];
175 let net_drift_lambda = self.beta * (self.mu - lambda) + self.alpha * lambda;
176
177 -0.5 * self.gamma * self.sigma.powi(2) * q.powi(2) + net_drift_lambda * derivs.grad[1]
180 }
181
182 fn terminal(&self, state: &[f64; 2]) -> f64 {
183 match self.terminal_condition {
184 TerminalCondition::Zero => 0.0,
185 TerminalCondition::LiquidationCost => {
186 let q = state[0];
187 let base_spread = (1.0 / self.gamma) * (1.0 + self.gamma / self.kappa).ln();
188 let half_spread = self.terminal_liquidation_half_spread.unwrap_or(base_spread);
189 -q.abs() * half_spread
190 }
191 }
192 }
193
194 fn discount_rate(&self, _state: &[f64; 2]) -> f64 {
195 0.0
196 }
197
198 fn constant_discount_rate(&self) -> Option<f64> {
199 Some(0.0)
200 }
201
202 fn next_step(&self, _t: f64, state: &[f64; 2], dt: f64, noise: &[f64; 2]) -> [f64; 2] {
203 let mut next = *state;
204
205 let u = normal_cdf(noise[0]);
206 let lambda = state[1].max(0.0);
207 let base_spread = (1.0 / self.gamma) * (1.0 + self.gamma / self.kappa).ln();
208 let lambda_bid = lambda * (-self.kappa * base_spread).exp();
209 let lambda_ask = lambda * (-self.kappa * base_spread).exp();
210 let p_bid = (lambda_bid * dt).clamp(0.0, 1.0);
211 let p_ask = (lambda_ask * dt).clamp(0.0, 1.0);
212 if u < p_bid {
213 next[0] += 1.0;
214 } else if u > 1.0 - p_ask {
215 next[0] -= 1.0;
216 }
217
218 let net_drift_lambda = self.beta * (self.mu - lambda) + self.alpha * lambda;
219 next[1] += net_drift_lambda * dt;
220 next[1] = next[1].max(0.0);
221 next
222 }
223
224 fn next_step_controlled(
225 &self,
226 _t: f64,
227 state: &[f64; 2],
228 control: &Self::Control,
229 dt: f64,
230 noise: &[f64; 2],
231 ) -> [f64; 2] {
232 let mut next = *state;
233
234 let u = normal_cdf(noise[0]);
237 let p_bid = (control.bid_intensity * dt).clamp(0.0, 1.0);
238 let p_ask = (control.ask_intensity * dt).clamp(0.0, 1.0);
239 if u < p_bid {
240 next[0] += 1.0;
241 } else if u > 1.0 - p_ask {
242 next[0] -= 1.0;
243 }
244
245 let lambda = state[1].max(0.0);
246 let net_drift_lambda = self.beta * (self.mu - lambda) + self.alpha * lambda;
247 next[1] += net_drift_lambda * dt;
248 next[1] = next[1].max(0.0);
249 next
250 }
251
252 fn is_diffusion_dimension(&self, _dim: usize) -> bool {
253 false
254 }
255
256 fn gradient_step(&self, dim: usize) -> f64 {
257 if dim == 1 { self.lambda_step } else { 1.0 }
258 }
259}
260
261impl PdeProblem<2> for AvellanedaHawkes {
262 fn dimension_kind(&self, dim: usize) -> DimensionKind {
263 match dim {
264 0 => DimensionKind::DiscreteJump,
265 _ => DimensionKind::DeterministicDrift,
266 }
267 }
268
269 fn transport(
270 &self,
271 _t: f64,
272 state: &[f64; 2],
273 control: &Self::Control,
274 derivs: &StateDerivatives<2>,
275 ) -> Transport<2> {
276 let q = state[0];
277 let lambda = state[1].max(0.0);
278 let net_drift = self.beta * (self.mu - lambda) + self.alpha * lambda;
279 let hamiltonian =
280 (control.bid_intensity + control.ask_intensity) / (self.gamma + self.kappa);
281 let jump_transport =
282 control.bid_intensity * derivs.fwd[0] - control.ask_intensity * derivs.bwd[0];
283 let local = -0.5 * self.gamma * self.sigma.powi(2) * q.powi(2);
284
285 let rate = net_drift.abs() / self.lambda_step.abs().max(1e-12);
286 let (plus_lam, minus_lam) = if net_drift >= 0.0 {
287 (rate, 0.0)
288 } else {
289 (0.0, rate)
290 };
291
292 Transport::new(
293 [control.bid_intensity, plus_lam],
294 [control.ask_intensity, minus_lam],
295 hamiltonian + local - jump_transport,
296 )
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 fn zero_derivs() -> StateDerivatives<2> {
305 StateDerivatives::new([0.0; 2], [0.0; 2])
306 }
307
308 fn default_model() -> AvellanedaHawkes {
309 AvellanedaHawkes::new(0.5, 0.5, 1.5, 0.5, 2.0, 1.0)
310 .with_lambda_step(0.1)
311 .with_dq(1.0)
312 }
313
314 #[test]
315 fn zero_gradient_gives_base_spread() {
316 let m = default_model();
317 let derivs = zero_derivs();
318 let (bid, ask) = m.get_spreads(1.0, &derivs);
319 let base = (1.0 / m.gamma) * (1.0 + m.gamma / m.kappa).ln();
320 assert!((bid - base).abs() < 1e-12);
321 assert!((ask - base).abs() < 1e-12);
322 }
323
324 #[test]
325 fn bsde_driver_excludes_intensity_transport() {
326 let m = default_model();
327 let state = [2.0, 1.0];
328 let control = ControlProblem::optimize(&m, 0.0, &state, &zero_derivs());
329
330 let mut derivs = zero_derivs();
331 derivs.grad[1] = 1.0;
332
333 let dt = 0.01;
334 let q = state[0];
335 let local = -0.5 * m.gamma * m.sigma.powi(2) * q.powi(2);
336 let rate_cap = 1.0 / dt;
337 let reward = (control.bid_intensity.min(rate_cap) + control.ask_intensity.min(rate_cap))
338 / (m.gamma + m.kappa);
339 let expected = reward + local;
340
341 assert_eq!(
342 ControlProblem::bsde_driver(&m, 0.0, &state, &control, &derivs, dt),
343 expected
344 );
345 assert!(
346 ControlProblem::bsde_driver(&m, 0.0, &state, &control, &derivs, dt)
347 != ControlProblem::driver(&m, 0.0, &state, &control, &derivs)
348 );
349 }
350
351 #[test]
352 fn next_step_controlled_uses_optimal_intensities() {
353 let m = default_model();
354 let state = [1.0, 1.0];
355
356 let ctrl = MarketMakingControl::new(0.0, 0.0);
357 let next = ControlProblem::next_step_controlled(&m, 0.0, &state, &ctrl, 0.01, &[0.0, 0.0]);
358 assert_eq!(next[0], 1.0);
359
360 let ctrl = MarketMakingControl::new(1000.0, 0.0);
361 let next = ControlProblem::next_step_controlled(&m, 0.0, &state, &ctrl, 0.01, &[0.0, 0.0]);
362 assert_eq!(next[0], 2.0);
363 }
364
365 #[test]
366 fn intensity_scales_fill_rates() {
367 let m = default_model();
368 let derivs = zero_derivs();
369 let ctrl_low = ControlProblem::optimize(&m, 0.0, &[0.0, 0.5], &derivs);
370 let ctrl_high = ControlProblem::optimize(&m, 0.0, &[0.0, 2.0], &derivs);
371 let total_low = ctrl_low.bid_intensity + ctrl_low.ask_intensity;
372 let total_high = ctrl_high.bid_intensity + ctrl_high.ask_intensity;
373 assert!(
374 total_high > total_low,
375 "Higher intensity should give higher fill rates: low={}, high={}",
376 total_low,
377 total_high
378 );
379 }
380
381 #[test]
382 fn subcritical_regime_mean_reverts() {
383 let m = AvellanedaHawkes::new(0.5, 0.5, 1.5, 0.5, 2.0, 1.0);
384 assert!(m.alpha < m.beta, "Subcritical: alpha < beta");
385
386 let eq = m.beta * m.mu / (m.beta - m.alpha);
388
389 let state_high = [0.0, eq + 1.0];
390 let next_high = ControlProblem::next_step(&m, 0.0, &state_high, 0.01, &[0.0, 0.0]);
391 assert!(
392 next_high[1] < state_high[1],
393 "Above equilibrium, intensity should decrease"
394 );
395
396 let state_low = [0.0, eq - 0.5];
397 let next_low = ControlProblem::next_step(&m, 0.0, &state_low, 0.01, &[0.0, 0.0]);
398 assert!(
399 next_low[1] > state_low[1],
400 "Below equilibrium, intensity should increase"
401 );
402 }
403
404 #[test]
405 fn intensity_stays_non_negative() {
406 let m = default_model();
407 let state = [0.0, 0.001];
408 let next = ControlProblem::next_step(&m, 0.0, &state, 0.01, &[0.0, 0.0]);
409 assert!(next[1] >= 0.0, "Intensity must stay non-negative");
410 }
411
412 #[test]
413 fn dimension_kinds_match_state_space() {
414 let m = default_model();
415 assert_eq!(
416 PdeProblem::dimension_kind(&m, 0),
417 DimensionKind::DiscreteJump
418 );
419 assert_eq!(
420 PdeProblem::dimension_kind(&m, 1),
421 DimensionKind::DeterministicDrift
422 );
423 }
424}