1use super::TerminalCondition;
2use super::normal_cdf;
3use super::traits::{ControlOutput, Gradients, Model};
4
5#[derive(Clone)]
21pub struct AvellanedaHawkes {
22 pub gamma: f64,
23 pub sigma: f64,
24 pub kappa: f64,
25 pub alpha: f64, pub beta: f64, pub mu: f64, pub lambda_step: f64,
30 pub dq: f64,
32 pub terminal_condition: TerminalCondition,
34 pub q_min: f64,
36 pub q_max: f64,
38 pub terminal_liquidation_half_spread: Option<f64>,
41}
42
43impl AvellanedaHawkes {
44 pub fn new(gamma: f64, sigma: f64, kappa: f64, alpha: f64, beta: f64, mu: f64) -> Self {
45 Self {
46 gamma,
47 sigma,
48 kappa,
49 alpha,
50 beta,
51 mu,
52 lambda_step: 1.0,
53 dq: 1.0,
54 terminal_condition: TerminalCondition::Zero,
55 q_min: f64::NEG_INFINITY,
56 q_max: f64::INFINITY,
57 terminal_liquidation_half_spread: None,
58 }
59 }
60
61 pub fn with_terminal_liquidation_half_spread(mut self, half_spread: f64) -> Self {
62 self.terminal_liquidation_half_spread = Some(half_spread.max(0.0));
63 self
64 }
65
66 pub fn with_inventory_bounds(mut self, q_min: f64, q_max: f64) -> Self {
67 self.q_min = q_min;
68 self.q_max = q_max;
69 self
70 }
71
72 pub fn with_terminal_condition(mut self, terminal_condition: TerminalCondition) -> Self {
73 self.terminal_condition = terminal_condition;
74 self
75 }
76
77 pub fn with_lambda_step(mut self, lambda_step: f64) -> Self {
78 self.lambda_step = lambda_step.abs().max(1e-8);
79 self
80 }
81
82 pub fn with_dq(mut self, dq: f64) -> Self {
83 self.dq = dq.abs().max(1e-8);
84 self
85 }
86
87 pub fn get_spreads(&self, _lambda: f64, grads: &Gradients<2>) -> (f64, f64) {
90 let base_spread = (1.0 / self.gamma) * (1.0 + self.gamma / self.kappa).ln();
95
96 let dv_dq_buy = grads.fwd[0];
97 let dv_dq_sell = grads.bwd[0];
98
99 let mut delta_bid = base_spread - dv_dq_buy;
100 let mut delta_ask = base_spread + dv_dq_sell;
101
102 let min_spread = -10.0;
103 let max_spread = 10.0;
104 delta_bid = delta_bid.max(min_spread).min(max_spread);
105 delta_ask = delta_ask.max(min_spread).min(max_spread);
106
107 (delta_bid, delta_ask)
108 }
109}
110
111impl Model<2> for AvellanedaHawkes {
112 type Process = ();
113
114 fn process(&self) {}
115
116 fn optimize(&self, state: &[f64; 2], grads: &Gradients<2>) -> ControlOutput<2> {
117 let q = state[0];
118 let lambda = state[1]; let (d_bid, d_ask) = self.get_spreads(lambda, grads);
122
123 let max_fill_mult = 20.0_f64;
126 let lambda_bid = lambda * (-self.kappa * d_bid).exp().min(max_fill_mult);
127 let lambda_ask = lambda * (-self.kappa * d_ask).exp().min(max_fill_mult);
128
129 let lambda_bid = if q >= self.q_max { 0.0 } else { lambda_bid };
131 let lambda_ask = if q <= self.q_min { 0.0 } else { lambda_ask };
132
133 let hamiltonian_inv = (lambda_bid + lambda_ask) / (self.gamma + self.kappa);
135
136 let risk_penalty = -0.5 * self.gamma * self.sigma.powi(2) * q.powi(2);
138
139 let drift_correction_q = self.dq * (lambda_bid * grads.fwd[0] - lambda_ask * grads.bwd[0]);
144
145 let net_drift_lambda = self.beta * (self.mu - lambda) + lambda * self.alpha;
148 let drift_abs = net_drift_lambda.abs() / self.lambda_step;
149
150 let (lambda_lam_plus, lambda_lam_minus) = if net_drift_lambda >= 0.0 {
151 (drift_abs, 0.0)
152 } else {
153 (0.0, drift_abs)
154 };
155
156 ControlOutput {
157 lambda_plus: [lambda_bid, lambda_lam_plus],
158 lambda_minus: [lambda_ask, lambda_lam_minus],
159 flow: hamiltonian_inv + risk_penalty - drift_correction_q,
160 }
161 }
162
163 fn terminal(&self, state: &[f64; 2]) -> f64 {
164 match self.terminal_condition {
165 TerminalCondition::Zero => 0.0,
166 TerminalCondition::LiquidationCost => {
167 let q = state[0];
168 let base_spread = (1.0 / self.gamma) * (1.0 + self.gamma / self.kappa).ln();
169 let half_spread = self.terminal_liquidation_half_spread.unwrap_or(base_spread);
170 -q.abs() * half_spread
171 }
172 }
173 }
174
175 fn constant_discount_rate(&self) -> Option<f64> {
176 Some(0.0)
177 }
178
179 fn next_step(&self, current_state: &[f64; 2], dt: f64, noise: &[f64; 2]) -> [f64; 2] {
180 let mut next = *current_state;
181 let lambda = current_state[1];
182 let net_drift_lambda = self.beta * (self.mu - lambda) + self.alpha * lambda;
183
184 let _ = noise;
187 next[1] += net_drift_lambda * dt;
188 next[1] = next[1].max(0.0);
189
190 next
191 }
192
193 fn next_step_controlled(
194 &self,
195 current_state: &[f64; 2],
196 control: &ControlOutput<2>,
197 dt: f64,
198 noise: &[f64; 2],
199 ) -> [f64; 2] {
200 let mut next = *current_state;
201 let lambda = current_state[1];
202
203 let u = normal_cdf(noise[0]);
208 let p_bid = (control.lambda_plus[0] * dt).clamp(0.0, 1.0);
209 let p_ask = (control.lambda_minus[0] * dt).clamp(0.0, 1.0);
210 if u < p_bid {
211 next[0] += 1.0; } else if u > 1.0 - p_ask {
213 next[0] -= 1.0; }
215
216 let net_drift_lambda = self.beta * (self.mu - lambda) + self.alpha * lambda;
218 let _ = noise[1];
219 next[1] += net_drift_lambda * dt;
220 next[1] = next[1].max(0.0);
221
222 next
223 }
224
225 fn is_diffusion_dimension(&self, _dim: usize) -> bool {
226 false }
228
229 fn is_integer_dimension(&self, dim: usize) -> bool {
230 dim == 0 }
232
233 fn gradient_step(&self, dim: usize) -> f64 {
234 if dim == 1 { self.lambda_step } else { 1.0 }
235 }
236
237 fn fill_rate_base(&self, state: &[f64; 2]) -> f64 {
240 state[1].max(1e-10)
241 }
242
243 fn fill_rate_decay(&self) -> f64 {
244 self.kappa
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::super::traits::{Gradients, Model};
251 use super::*;
252
253 fn zero_grads() -> Gradients<2> {
254 Gradients {
255 fwd: [0.0; 2],
256 bwd: [0.0; 2],
257 }
258 }
259
260 fn default_model() -> AvellanedaHawkes {
261 AvellanedaHawkes::new(0.5, 0.5, 1.5, 0.5, 2.0, 1.0)
262 .with_lambda_step(0.1)
263 .with_dq(1.0)
264 }
265
266 #[test]
267 fn zero_gradient_gives_base_spread() {
268 let m = default_model();
269 let grads = zero_grads();
270 let (bid, ask) = m.get_spreads(1.0, &grads);
271 let base = (1.0 / m.gamma) * (1.0 + m.gamma / m.kappa).ln();
272 assert!((bid - base).abs() < 1e-12);
273 assert!((ask - base).abs() < 1e-12);
274 }
275
276 #[test]
277 fn intensity_scales_fill_rates() {
278 let m = default_model();
279 let grads = zero_grads();
280 let ctrl_low = m.optimize(&[0.0, 0.5], &grads);
281 let ctrl_high = m.optimize(&[0.0, 2.0], &grads);
282 let total_low = ctrl_low.lambda_plus[0] + ctrl_low.lambda_minus[0];
283 let total_high = ctrl_high.lambda_plus[0] + ctrl_high.lambda_minus[0];
284 assert!(
285 total_high > total_low,
286 "Higher intensity should give higher fill rates: low={}, high={}",
287 total_low,
288 total_high
289 );
290 }
291
292 #[test]
293 fn subcritical_regime_mean_reverts() {
294 let m = AvellanedaHawkes::new(0.5, 0.5, 1.5, 0.5, 2.0, 1.0);
295 assert!(m.alpha < m.beta, "Subcritical: alpha < beta");
296
297 let eq = m.beta * m.mu / (m.beta - m.alpha);
299
300 let state_high = [0.0, eq + 1.0];
301 let next_high = m.next_step(&state_high, 0.01, &[0.0, 0.0]);
302 assert!(
303 next_high[1] < state_high[1],
304 "Above equilibrium, intensity should decrease"
305 );
306
307 let state_low = [0.0, eq - 0.5];
308 let next_low = m.next_step(&state_low, 0.01, &[0.0, 0.0]);
309 assert!(
310 next_low[1] > state_low[1],
311 "Below equilibrium, intensity should increase"
312 );
313 }
314
315 #[test]
316 fn intensity_drift_uses_upwinding() {
317 let m = default_model();
318 let grads = zero_grads();
319 let ctrl = m.optimize(&[0.0, 0.1], &grads);
320 assert!(
321 ctrl.lambda_plus[1] > 0.0,
322 "Positive drift should give lambda_plus > 0"
323 );
324 assert!(
325 ctrl.lambda_minus[1] < 1e-12,
326 "Positive drift should give lambda_minus ~ 0"
327 );
328 }
329
330 #[test]
331 fn intensity_stays_non_negative() {
332 let m = default_model();
333 let state = [0.0, 0.001];
334 let next = m.next_step(&state, 0.01, &[0.0, 0.0]);
335 assert!(next[1] >= 0.0, "Intensity must stay non-negative");
336 }
337
338 #[test]
339 fn alpha_zero_makes_drift_simple_mean_reversion() {
340 let m = AvellanedaHawkes::new(0.5, 0.5, 1.5, 0.0, 2.0, 1.0).with_lambda_step(0.1);
341 let grads = zero_grads();
342 let ctrl = m.optimize(&[0.0, m.mu], &grads);
344 assert!(
345 ctrl.lambda_plus[1].abs() < 1e-10 && ctrl.lambda_minus[1].abs() < 1e-10,
346 "At lambda=mu with alpha=0, net drift should be zero"
347 );
348 }
349}