1use 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::elliptic::EllipticControlProblem;
7use crate::numeric::finite_difference::pde::PdeProblem;
8
9#[derive(Clone)]
19pub struct AvellanedaStoikov {
20 pub gamma: f64,
22 pub sigma: f64,
24 pub kappa: f64,
26 pub a: f64,
28}
29
30impl AvellanedaStoikov {
31 pub fn get_spreads(&self, derivs: &StateDerivatives<2>) -> (f64, f64) {
36 let base_spread = (1.0 / self.gamma) * (1.0 + self.gamma / self.kappa).ln();
37
38 let dv_dq_buy = derivs.fwd[0];
39 let dv_dq_sell = derivs.bwd[0];
40
41 let mut delta_bid = base_spread - dv_dq_buy;
42 let mut delta_ask = base_spread + dv_dq_sell;
43
44 let min_spread = -10.0;
46 delta_bid = delta_bid.max(min_spread);
47 delta_ask = delta_ask.max(min_spread);
48
49 (delta_bid, delta_ask)
50 }
51
52 pub fn fill_rate_base(&self, _state: &[f64; 2]) -> f64 {
54 self.a
55 }
56
57 pub fn fill_rate_decay(&self) -> f64 {
59 self.kappa
60 }
61}
62
63impl ControlProblem<2> for AvellanedaStoikov {
64 type Control = MarketMakingControl;
65
66 fn optimize(&self, _t: f64, _state: &[f64; 2], derivs: &StateDerivatives<2>) -> Self::Control {
67 let base_spread = (1.0 / self.gamma) * (1.0 + self.gamma / self.kappa).ln();
68 let delta_bid = (base_spread - derivs.fwd[0]).max(-10.0);
69 let delta_ask = (base_spread + derivs.bwd[0]).max(-10.0);
70
71 MarketMakingControl::new(
72 self.a * (-self.kappa * delta_bid).exp(),
73 self.a * (-self.kappa * delta_ask).exp(),
74 )
75 }
76
77 fn running_reward(&self, _t: f64, _state: &[f64; 2], control: &Self::Control) -> f64 {
78 (control.bid_intensity + control.ask_intensity) / (self.gamma + self.kappa)
80 }
81
82 fn bsde_driver(
83 &self,
84 t: f64,
85 state: &[f64; 2],
86 control: &Self::Control,
87 derivs: &StateDerivatives<2>,
88 _dt: f64,
89 ) -> f64 {
90 self.driver(t, state, control, derivs)
94 }
95
96 fn generator(
97 &self,
98 _t: f64,
99 state: &[f64; 2],
100 _control: &Self::Control,
101 _derivs: &StateDerivatives<2>,
102 ) -> f64 {
103 -0.5 * self.gamma * self.sigma.powi(2) * state[0].powi(2)
105 }
106
107 fn terminal(&self, _state: &[f64; 2]) -> f64 {
108 0.0
109 }
110
111 fn discount_rate(&self, _state: &[f64; 2]) -> f64 {
112 0.0
113 }
114
115 fn constant_discount_rate(&self) -> Option<f64> {
116 Some(0.0)
117 }
118
119 fn next_step(&self, _t: f64, state: &[f64; 2], dt: f64, noise: &[f64; 2]) -> [f64; 2] {
120 let mut next = *state;
121
122 let u = normal_cdf(noise[0]);
124 let base_spread = (1.0 / self.gamma) * (1.0 + self.gamma / self.kappa).ln();
125 let delta_bid = base_spread;
126 let delta_ask = base_spread;
127 let lambda_bid = self.a * (-self.kappa * delta_bid).exp();
128 let lambda_ask = self.a * (-self.kappa * delta_ask).exp();
129 let p_bid = (lambda_bid * dt).clamp(0.0, 1.0);
130 let p_ask = (lambda_ask * dt).clamp(0.0, 1.0);
131 if u < p_bid {
132 next[0] += 1.0;
133 } else if u > 1.0 - p_ask {
134 next[0] -= 1.0;
135 }
136
137 next[1] += self.sigma * dt.sqrt() * noise[1];
139 next
140 }
141
142 fn is_reduced_value(&self) -> bool {
143 true
144 }
145
146 fn is_diffusion_dimension(&self, dim: usize) -> bool {
147 dim == 1
148 }
149
150 fn gradient_step(&self, _dim: usize) -> f64 {
151 1.0
152 }
153}
154
155impl PdeProblem<2> for AvellanedaStoikov {
156 fn dimension_kind(&self, dim: usize) -> DimensionKind {
157 match dim {
158 0 => DimensionKind::DiscreteJump,
159 _ => DimensionKind::Diffusion,
160 }
161 }
162
163 fn transport(
164 &self,
165 _t: f64,
166 state: &[f64; 2],
167 control: &Self::Control,
168 derivs: &StateDerivatives<2>,
169 ) -> Transport<2> {
170 let q = state[0];
174 let hamiltonian =
175 (control.bid_intensity + control.ask_intensity) / (self.gamma + self.kappa);
176 let jump_transport =
177 control.bid_intensity * derivs.fwd[0] - control.ask_intensity * derivs.bwd[0];
178 let local = -0.5 * self.gamma * self.sigma.powi(2) * q.powi(2);
179 Transport::new(
180 [control.bid_intensity, 0.0],
181 [control.ask_intensity, 0.0],
182 hamiltonian + local - jump_transport,
183 )
184 }
185}
186
187impl EllipticControlProblem<2> for AvellanedaStoikov {
188 fn dimension_kind(&self, dim: usize) -> DimensionKind {
189 <Self as PdeProblem<2>>::dimension_kind(self, dim)
190 }
191
192 fn transport(
193 &self,
194 state: &[f64; 2],
195 control: &Self::Control,
196 derivs: &StateDerivatives<2>,
197 ) -> Transport<2> {
198 <Self as PdeProblem<2>>::transport(self, 0.0, state, control, derivs)
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 fn zero_derivs() -> StateDerivatives<2> {
207 StateDerivatives::new([0.0; 2], [0.0; 2])
208 }
209
210 fn default_model() -> AvellanedaStoikov {
211 AvellanedaStoikov {
212 gamma: 0.1,
213 sigma: 0.1,
214 kappa: 1.5,
215 a: 140.0,
216 }
217 }
218
219 #[test]
220 fn zero_gradient_gives_base_spread() {
221 let m = default_model();
222 let derivs = zero_derivs();
223 let (bid, ask) = m.get_spreads(&derivs);
224 let base = (1.0 / m.gamma) * (1.0 + m.gamma / m.kappa).ln();
225 assert!((bid - base).abs() < 1e-12);
226 assert!((ask - base).abs() < 1e-12);
227 }
228
229 #[test]
230 fn symmetric_at_zero_inventory() {
231 let m = default_model();
232 let derivs = zero_derivs();
233 let ctrl = ControlProblem::optimize(&m, 0.0, &[0.0, 100.0], &derivs);
234 assert!(
235 (ctrl.bid_intensity - ctrl.ask_intensity).abs() < 1e-12,
236 "Bid/ask intensities should be equal at q=0"
237 );
238 }
239
240 #[test]
241 fn terminal_is_zero() {
242 let m = default_model();
243 assert_eq!(ControlProblem::terminal(&m, &[0.0, 100.0]), 0.0);
244 assert_eq!(ControlProblem::terminal(&m, &[5.0, 200.0]), 0.0);
245 }
246
247 #[test]
248 fn dim1_is_diffusion() {
249 let m = default_model();
250 assert!(!ControlProblem::is_diffusion_dimension(&m, 0));
251 assert!(ControlProblem::is_diffusion_dimension(&m, 1));
252 }
253
254 #[test]
255 fn dimension_kinds_match_state_space() {
256 let m = default_model();
257 assert_eq!(
258 PdeProblem::dimension_kind(&m, 0),
259 DimensionKind::DiscreteJump
260 );
261 assert_eq!(PdeProblem::dimension_kind(&m, 1), DimensionKind::Diffusion);
262 }
263
264 #[test]
265 fn transport_source_reconstructs_full_driver() {
266 let m = default_model();
267 let state = [3.0, 100.0];
268 let derivs =
269 StateDerivatives::with_directional([0.1, 0.0], [0.0, 0.0], [0.25, 0.0], [0.15, 0.0]);
270
271 let control = ControlProblem::optimize(&m, 0.0, &state, &derivs);
272 let transport = PdeProblem::transport(&m, 0.0, &state, &control, &derivs);
273
274 let operator_driver = transport.source
279 + transport.plus[0] * derivs.fwd[0]
280 + transport.minus[0] * (-derivs.bwd[0]);
281
282 let expected = ControlProblem::driver(&m, 0.0, &state, &control, &derivs);
283 assert!(
284 (operator_driver - expected).abs() < 1e-12,
285 "transport decomposition {operator_driver} does not match driver {expected}"
286 );
287 }
288}