1use super::Strategy;
2use crate::types::{Observation, Order, OrderRequest, Side};
3use solver::lookup::LookupTable;
4use std::path::Path;
5use std::sync::Arc;
6
7#[derive(Clone, Debug, Default)]
13pub struct AvellanedaStoikovHawkesParameters {
14 pub t_horizon: f64,
16 pub dt: f64,
18 pub base_intensity: f64,
21}
22
23#[derive(Clone)]
39pub struct AvellanedaStoikovHawkesStrategy {
40 pub params: AvellanedaStoikovHawkesParameters,
41 pub bid_spread_table: Arc<LookupTable>,
43 pub ask_spread_table: Arc<LookupTable>,
45 start_time: Option<f64>,
46 order_counter: u64,
47}
48
49impl AvellanedaStoikovHawkesStrategy {
50 pub fn new(
51 params: AvellanedaStoikovHawkesParameters,
52 bid_spread_table: Arc<LookupTable>,
53 ask_spread_table: Arc<LookupTable>,
54 ) -> Self {
55 assert_eq!(
56 bid_spread_table.axes.len(),
57 3,
58 "bid table must be 3D [q, lambda, tau]"
59 );
60 assert_eq!(
61 ask_spread_table.axes.len(),
62 3,
63 "ask table must be 3D [q, lambda, tau]"
64 );
65 Self {
66 params,
67 bid_spread_table,
68 ask_spread_table,
69 start_time: None,
70 order_counter: 0,
71 }
72 }
73
74 pub fn load(
77 params: AvellanedaStoikovHawkesParameters,
78 bid_path: &Path,
79 ask_path: &Path,
80 ) -> Result<Self, Box<dyn std::error::Error>> {
81 let bid_spread_table = Arc::new(LookupTable::load(bid_path)?);
82 let ask_spread_table = Arc::new(LookupTable::load(ask_path)?);
83 Ok(Self::new(params, bid_spread_table, ask_spread_table))
84 }
85}
86
87impl Strategy for AvellanedaStoikovHawkesStrategy {
88 fn on_tick(&mut self, obs: &Observation, requests: &mut Vec<OrderRequest>) {
89 if self.start_time.is_none() {
90 self.start_time = Some(obs.timestamp);
91 }
92
93 let elapsed = obs.timestamp - self.start_time.unwrap();
94 let tau = (self.params.t_horizon - elapsed).max(0.0);
95
96 if tau <= 0.0 {
97 requests.push(OrderRequest::CancelAll);
98 return;
99 }
100
101 let mid = obs.mid_price();
102 let q = obs.portfolio.position;
103
104 let params = obs.parameters.as_ref();
113 let unified = params.and_then(|p| p.get("hawkes_intensity").copied());
114 let lambda_bid = params
115 .and_then(|p| p.get("hawkes_sell_intensity").copied())
116 .or(unified)
117 .unwrap_or(self.params.base_intensity);
118 let lambda_ask = params
119 .and_then(|p| p.get("hawkes_buy_intensity").copied())
120 .or(unified)
121 .unwrap_or(self.params.base_intensity);
122
123 let delta_bid = self
124 .bid_spread_table
125 .interpolate(&[q, lambda_bid, tau])
126 .max(0.0);
127 let delta_ask = self
128 .ask_spread_table
129 .interpolate(&[q, lambda_ask, tau])
130 .max(0.0);
131
132 let bid_price = ((mid - delta_bid) * 100.0).round() / 100.0;
133 let ask_price = ((mid + delta_ask) * 100.0).round() / 100.0;
134
135 self.order_counter += 1;
136 let bid_id = self.order_counter;
137 self.order_counter += 1;
138 let ask_id = self.order_counter;
139
140 requests.push(OrderRequest::CancelAll);
141 requests.push(OrderRequest::New(Order::new(
142 bid_id,
143 Side::Buy,
144 bid_price,
145 1.0,
146 )));
147 requests.push(OrderRequest::New(Order::new(
148 ask_id,
149 Side::Sell,
150 ask_price,
151 1.0,
152 )));
153 }
154
155 fn as_any(&self) -> &dyn std::any::Any {
156 self
157 }
158 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
159 self
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use crate::types::PortfolioSnapshot;
167 use ndarray::{ArrayD, IxDyn};
168 use solver::lookup::LookupTable;
169
170 fn flat_table(axes: Vec<Vec<f64>>, value: f64) -> LookupTable {
171 let shape: Vec<usize> = axes.iter().map(|a| a.len()).collect();
172 let n: usize = shape.iter().product();
173 let data = ArrayD::from_shape_vec(IxDyn(&shape), vec![value; n]).unwrap();
174 LookupTable::new(axes, data)
175 }
176
177 fn make_obs(position: f64, intensity: f64) -> Observation {
178 let mut params = std::collections::HashMap::new();
179 params.insert("hawkes_intensity".to_string(), intensity);
180 Observation {
181 timestamp: 0.0,
182 best_bid: 99.95,
183 best_ask: 100.05,
184 last_price: Some(100.0),
185 portfolio: PortfolioSnapshot {
186 cash: 10000.0,
187 position,
188 },
189 volatility: None,
190 drift: None,
191 parameters: Some(params),
192 }
193 }
194
195 #[test]
196 fn test_places_quotes_at_flat_spread() {
197 let axes = vec![
198 vec![-1.0, 0.0, 1.0], vec![0.5, 1.0, 2.0], vec![0.0, 500.0, 1000.0], ];
202 let bid_table = flat_table(axes.clone(), 0.05);
203 let ask_table = flat_table(axes, 0.05);
204 let params = AvellanedaStoikovHawkesParameters {
205 t_horizon: 1000.0,
206 dt: 1.0,
207 base_intensity: 1.0,
208 };
209 let mut strategy =
210 AvellanedaStoikovHawkesStrategy::new(params, Arc::new(bid_table), Arc::new(ask_table));
211 let obs = make_obs(0.0, 1.0);
212
213 let mut requests = Vec::new();
214 strategy.on_tick(&obs, &mut requests);
215
216 assert_eq!(requests.len(), 3); let bid = match &requests[1] {
218 OrderRequest::New(o) => {
219 assert_eq!(o.side, Side::Buy);
220 o.limit_price
221 }
222 _ => panic!("expected bid order"),
223 };
224 let ask = match &requests[2] {
225 OrderRequest::New(o) => {
226 assert_eq!(o.side, Side::Sell);
227 o.limit_price
228 }
229 _ => panic!("expected ask order"),
230 };
231 assert!((bid - 99.95).abs() < 0.01);
232 assert!((ask - 100.05).abs() < 0.01);
233 }
234
235 #[test]
236 fn test_fallback_to_base_intensity_when_params_absent() {
237 let axes = vec![vec![0.0], vec![1.0], vec![0.0, 1000.0]];
238 let bid_table = flat_table(axes.clone(), 0.03);
239 let ask_table = flat_table(axes, 0.03);
240 let params = AvellanedaStoikovHawkesParameters {
241 t_horizon: 1000.0,
242 dt: 1.0,
243 base_intensity: 1.0,
244 };
245 let mut strategy =
246 AvellanedaStoikovHawkesStrategy::new(params, Arc::new(bid_table), Arc::new(ask_table));
247 let obs_no_params = Observation {
248 timestamp: 0.0,
249 best_bid: 99.95,
250 best_ask: 100.05,
251 last_price: None,
252 portfolio: PortfolioSnapshot {
253 cash: 10000.0,
254 position: 0.0,
255 },
256 volatility: None,
257 drift: None,
258 parameters: None,
259 };
260 let mut requests = Vec::new();
261 strategy.on_tick(&obs_no_params, &mut requests);
262 assert_eq!(requests.len(), 3);
263 }
264
265 #[test]
266 fn test_cancels_at_end_of_horizon() {
267 let axes = vec![vec![0.0], vec![1.0], vec![0.0]];
268 let t = Arc::new(flat_table(axes.clone(), 0.05));
269 let params = AvellanedaStoikovHawkesParameters {
270 t_horizon: 0.0,
271 dt: 1.0,
272 base_intensity: 1.0,
273 };
274 let mut strategy = AvellanedaStoikovHawkesStrategy::new(params, Arc::clone(&t), t);
275 let obs = make_obs(0.0, 1.0);
276 let mut requests = Vec::new();
277 strategy.on_tick(&obs, &mut requests);
278 assert_eq!(requests.len(), 1);
279 assert!(matches!(requests[0], OrderRequest::CancelAll));
280 }
281
282 #[test]
283 fn test_bilateral_hawkes_uses_side_specific_intensities() {
284 let axes = vec![
289 vec![-1.0, 0.0, 1.0],
290 vec![0.0, 1.0, 3.0],
291 vec![0.0, 500.0, 1000.0],
292 ];
293 let bid_table = Arc::new(flat_table(axes.clone(), 0.10));
294 let ask_table = Arc::new(flat_table(axes, 0.20));
295 let params = AvellanedaStoikovHawkesParameters {
296 t_horizon: 1000.0,
297 dt: 1.0,
298 base_intensity: 1.0,
299 };
300 let mut strategy = AvellanedaStoikovHawkesStrategy::new(params, bid_table, ask_table);
301
302 let mut bilateral_params = std::collections::HashMap::new();
303 bilateral_params.insert("hawkes_buy_intensity".to_string(), 2.0);
304 bilateral_params.insert("hawkes_sell_intensity".to_string(), 0.5);
305 let obs = Observation {
306 timestamp: 0.0,
307 best_bid: 99.95,
308 best_ask: 100.05,
309 last_price: Some(100.0),
310 portfolio: PortfolioSnapshot {
311 cash: 10000.0,
312 position: 0.0,
313 },
314 volatility: None,
315 drift: None,
316 parameters: Some(bilateral_params),
317 };
318
319 let mut requests = Vec::new();
320 strategy.on_tick(&obs, &mut requests);
321
322 let bid_price = match &requests[1] {
323 OrderRequest::New(o) => o.limit_price,
324 _ => panic!("expected bid order"),
325 };
326 let ask_price = match &requests[2] {
327 OrderRequest::New(o) => o.limit_price,
328 _ => panic!("expected ask order"),
329 };
330 assert!((bid_price - 99.90).abs() < 0.01, "bid_price={bid_price}");
332 assert!((ask_price - 100.20).abs() < 0.01, "ask_price={ask_price}");
333 }
334}