solver/analytical/market_impact/
exact.rs1use crate::analytical::traits::AnalyticalSolution;
2use crate::models::traits::ControlOutput;
3use crate::numeric::ode::LinearSpectralSolver;
4
5pub struct AvellanedaImpactExact {
8 pub gamma: f64,
9 pub sigma: f64,
10 pub kappa: f64,
11 pub a: f64,
12 pub xi: f64, pub phi: f64, pub terminal_time: f64,
15 pub q_max: usize,
16}
17
18impl AvellanedaImpactExact {
19 #[allow(clippy::too_many_arguments)]
20 pub fn new(
21 gamma: f64,
22 sigma: f64,
23 kappa: f64,
24 a: f64,
25 xi: f64,
26 phi: f64,
27 terminal_time: f64,
28 q_max: usize,
29 ) -> Self {
30 Self {
31 gamma,
32 sigma,
33 kappa,
34 a,
35 xi,
36 phi,
37 terminal_time,
38 q_max,
39 }
40 }
41
42 fn compute_z_and_alpha(&self, t: f64) -> (Vec<f64>, f64) {
50 let n = 2 * self.q_max + 1;
51 let time_remaining = self.terminal_time - t;
52 let alpha_trans = 0.5 * self.kappa * self.xi; if time_remaining <= 1e-9 {
55 let mut z_t = vec![0.0; n];
61 for (i, item) in z_t.iter_mut().enumerate() {
62 let q_val = (i as i32) - (self.q_max as i32);
63 let q = q_val as f64;
64 *item = (-self.kappa * self.xi * q.powi(2)).exp();
65 }
66 return (z_t, alpha_trans);
67 }
68
69 let alpha_risk = self.kappa * (0.5 * self.gamma * self.sigma.powi(2) + self.phi);
71 let eta = self.a * (1.0 + self.gamma / self.kappa).powf(-(1.0 + self.kappa / self.gamma));
72
73 let mut d = vec![0.0; n];
74 let mut e = vec![0.0; n];
75
76 let off_diag_val = eta * (-alpha_trans).exp();
81
82 for i in 0..n {
88 let q_val = (i as i32) - (self.q_max as i32);
89 let q = q_val as f64;
90 d[i] = -alpha_risk * q.powi(2);
92
93 if i < n - 1 {
94 e[i] = off_diag_val;
95 }
96 }
97
98 let mut z0 = vec![0.0; n];
101 for (i, item) in z0.iter_mut().enumerate() {
102 let q_val = (i as i32) - (self.q_max as i32);
103 let q = q_val as f64;
104 *item = (-self.kappa * self.xi * q.powi(2)).exp();
105 }
106
107 let z_t = LinearSpectralSolver::solve_tridiagonal(d, e, &z0, time_remaining);
109
110 (z_t, alpha_trans)
111 }
112
113 pub fn exact_spreads(&self, t: f64, q: f64) -> (f64, f64) {
115 let (z_t, alpha_trans) = self.compute_z_and_alpha(t);
116 let n = z_t.len();
117
118 for val in &z_t {
120 if val.is_nan() {
121 return (f64::NAN, f64::NAN);
122 }
123 }
124
125 let const_term = (1.0 / self.gamma) * (1.0 + self.gamma / self.kappa).ln();
126 let q_idx = (q.round() as i32 + self.q_max as i32) as usize;
127
128 if q_idx == 0 || q_idx >= n - 1 {
129 return (999.0, 999.0);
130 }
131
132 let z_q = z_t[q_idx].max(1e-50);
134 let z_qp1 = z_t[q_idx + 1].max(1e-50);
135 let z_qm1 = z_t[q_idx - 1].max(1e-50);
136
137 let ln_ratio_bid = z_q.ln() - z_qp1.ln() - alpha_trans * (2.0 * q + 1.0);
140
141 let ln_ratio_ask = z_q.ln() - z_qm1.ln() + alpha_trans * (2.0 * q - 1.0);
144
145 let bid = (1.0 / self.kappa) * ln_ratio_bid + (q + 1.0) * self.xi + const_term;
146 let ask = (1.0 / self.kappa) * ln_ratio_ask - (q - 1.0) * self.xi + const_term;
147
148 (bid, ask)
149 }
150}
151
152impl AnalyticalSolution<2> for AvellanedaImpactExact {
153 fn value_function(&self, t: f64, state: &[f64; 2]) -> f64 {
154 let q = state[0];
155 let (z_t, alpha_trans) = self.compute_z_and_alpha(t);
156 let q_idx = (q.round() as i32 + self.q_max as i32) as usize;
157
158 if q_idx >= z_t.len() {
159 return 0.0;
160 }
161
162 (1.0 / self.kappa) * (alpha_trans * q.powi(2) + z_t[q_idx].ln())
165 }
166
167 fn optimal_controls(&self, t: f64, state: &[f64; 2]) -> ControlOutput<2> {
168 let q = state[0];
169 let (d_bid, d_ask) = self.exact_spreads(t, q);
170
171 let lambda_bid = self.a * (-self.kappa * d_bid).exp();
172 let lambda_ask = self.a * (-self.kappa * d_ask).exp();
173
174 let flow = lambda_bid * d_bid + lambda_ask * d_ask;
175
176 ControlOutput {
177 lambda_plus: [lambda_bid, 0.0],
178 lambda_minus: [lambda_ask, 0.0],
179 flow,
180 }
181 }
182}