solver/analytical/avellaneda/approximations/
gueant.rs1use crate::analytical::traits::AnalyticalSolution;
2use crate::linalg::eigen::eigen_symmetric_tridiagonal;
3use crate::models::market_making::MarketMakingControl;
4
5pub struct AvellanedaGueant {
10 pub gamma: f64,
11 pub sigma: f64,
12 pub kappa: f64,
13 pub a: f64,
14 pub q_max: usize,
16}
17
18impl AvellanedaGueant {
19 pub fn new(gamma: f64, sigma: f64, kappa: f64, a: f64) -> Self {
20 Self {
21 gamma,
22 sigma,
23 kappa,
24 a,
25 q_max: 50,
26 }
27 }
28
29 pub fn with_q_max(mut self, q_max: usize) -> Self {
31 assert!(q_max > 0, "q_max must be positive");
32 self.q_max = q_max;
33 self
34 }
35
36 pub fn approximate_spreads(&self, q: f64) -> (f64, f64) {
46 let risk_factor = (self.sigma.powi(2) * self.gamma / (2.0 * self.kappa * self.a)).sqrt();
47 let const_term = (1.0 / self.gamma) * (1.0 + self.gamma / self.kappa).ln();
48 let correction =
49 (1.0 + self.gamma / self.kappa).powf(0.5 * (1.0 + self.kappa / self.gamma));
50
51 let delta_bid = const_term + (2.0 * q + 1.0) / 2.0 * risk_factor * correction;
52 let delta_ask = const_term - (2.0 * q - 1.0) / 2.0 * risk_factor * correction;
53
54 (delta_bid, delta_ask)
55 }
56
57 pub fn stationary_theta(&self, q_max: usize, q: i32) -> f64 {
78 let n = 2 * q_max + 1;
79 assert!(
80 q >= -(q_max as i32) && q <= q_max as i32,
81 "inventory q must lie within [-q_max, q_max]"
82 );
83 let idx = (q + q_max as i32) as usize;
84 assert!(idx < n, "index {idx} out of range for n={n}");
85
86 let alpha = (self.kappa / 2.0) * self.gamma * self.sigma.powi(2);
87 let eta = self.a * (1.0 + self.gamma / self.kappa).powf(-(1.0 + self.kappa / self.gamma));
88
89 let mut d = vec![0.0; n];
90 let mut e = vec![0.0; n - 1];
91 for i in 0..n {
92 let q_val = (i as i32) - (q_max as i32);
93 d[i] = -alpha * (q_val as f64).powi(2);
94 if i < n - 1 {
95 e[i] = eta;
96 }
97 }
98
99 let (_evals, evecs) = eigen_symmetric_tridiagonal(d, e);
100 let k = n - 1;
103 let v_q = evecs[idx + k * n];
104 let v_0 = evecs[q_max + k * n];
105
106 (1.0 / self.kappa) * (v_q / v_0).ln()
107 }
108
109 pub fn stationary_spreads(&self, q_max: usize, q: i32) -> (f64, f64) {
115 let base = (1.0 / self.gamma) * (1.0 + self.gamma / self.kappa).ln();
116 let theta_q = self.stationary_theta(q_max, q);
117
118 let theta_up = if q < q_max as i32 {
121 self.stationary_theta(q_max, q + 1)
122 } else {
123 theta_q
124 };
125 let theta_dn = if q > -(q_max as i32) {
126 self.stationary_theta(q_max, q - 1)
127 } else {
128 theta_q
129 };
130
131 let bid = base + (theta_q - theta_up);
132 let ask = base + (theta_q - theta_dn);
133 (bid, ask)
134 }
135}
136
137impl AnalyticalSolution<2> for AvellanedaGueant {
138 fn value_function(&self, _t: f64, state: &[f64; 2]) -> f64 {
139 self.stationary_theta(self.q_max, state[0].round() as i32)
140 }
141
142 fn optimal_controls(&self, _t: f64, state: &[f64; 2]) -> MarketMakingControl {
143 let q = state[0];
144 let (d_bid, d_ask) = self.approximate_spreads(q);
145
146 let lambda_bid = self.a * (-self.kappa * d_bid).exp();
147 let lambda_ask = self.a * (-self.kappa * d_ask).exp();
148
149 MarketMakingControl::new(lambda_bid, lambda_ask)
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 fn default_gueant() -> AvellanedaGueant {
158 AvellanedaGueant::new(0.1, 0.1, 1.5, 140.0).with_q_max(50)
159 }
160
161 #[test]
162 fn stationary_theta_is_zero_at_zero_inventory() {
163 let gueant = default_gueant();
164 let theta = gueant.stationary_theta(50, 0);
165 assert!((theta - 0.0).abs() < 1e-12);
166 }
167
168 #[test]
169 fn stationary_theta_decreases_with_inventory_magnitude() {
170 let gueant = default_gueant();
171 let theta0 = gueant.stationary_theta(50, 0);
172 let theta1 = gueant.stationary_theta(50, 1);
173 let theta2 = gueant.stationary_theta(50, 2);
174 assert!(theta1 < theta0);
175 assert!(theta2 < theta1);
176 }
177
178 #[test]
179 fn stationary_spreads_match_small_gamma_approximation() {
180 let gueant = default_gueant();
181 let q = 3;
182 let (bid_approx, ask_approx) = gueant.approximate_spreads(q as f64);
183 let (bid, ask) = gueant.stationary_spreads(50, q);
184
185 assert!((bid - bid_approx).abs() < 1e-2, "bid {bid} vs {bid_approx}");
188 assert!((ask - ask_approx).abs() < 1e-2, "ask {ask} vs {ask_approx}");
189 }
190}