1use super::Strategy;
2use crate::types::{Observation, Order, OrderRequest, Side};
3use std::collections::VecDeque;
4
5#[derive(Clone, Debug)]
6pub struct AvellanedaStoikovParameters {
7 pub gamma: f64, pub sigma: f64, pub t_horizon: f64, pub k: f64, pub a: f64, pub dt: f64, pub allow_real_vol: bool, pub enable_vol_estimation: bool, pub impact_parameter: f64, }
17
18pub struct AvellanedaStoikovStrategy {
19 pub params: AvellanedaStoikovParameters,
21
22 pub sigma: f64, pub start_time: Option<f64>,
25 pub order_counter: u64,
26
27 pub price_history: VecDeque<f64>,
29 pub window_size: usize,
30}
31
32impl AvellanedaStoikovStrategy {
33 pub fn new(params: AvellanedaStoikovParameters) -> Self {
34 let sigma = params.sigma;
35 Self {
36 params,
37 sigma,
38 start_time: None,
39 order_counter: 0,
40 price_history: VecDeque::new(),
41 window_size: 100, }
43 }
44
45 pub fn with_window_size(mut self, window_size: usize) -> Self {
46 self.window_size = window_size;
47 self
48 }
49
50 fn update_volatility(&mut self, price: f64) {
51 self.price_history.push_back(price);
52 if self.price_history.len() > self.window_size {
53 self.price_history.pop_front();
54 }
55
56 if self.price_history.len() >= 3 {
57 let mut log_returns = Vec::with_capacity(self.price_history.len() - 1);
59 for i in 0..self.price_history.len() - 1 {
60 let r = (self.price_history[i + 1] / self.price_history[i]).ln();
61 log_returns.push(r);
62 }
63
64 let mean = log_returns.iter().sum::<f64>() / log_returns.len() as f64;
65 let variance = log_returns.iter().map(|r| (r - mean).powi(2)).sum::<f64>()
66 / (log_returns.len() - 1) as f64;
67
68 let sigma_step = variance.sqrt();
69
70 let alpha = 0.1;
72 self.sigma = (1.0 - alpha) * self.sigma + alpha * sigma_step;
73 }
74 }
75}
76
77impl Strategy for AvellanedaStoikovStrategy {
78 fn on_tick(&mut self, obs: &Observation, requests: &mut Vec<OrderRequest>) {
79 if self.start_time.is_none() {
81 self.start_time = Some(obs.timestamp);
82 }
83
84 let mid_price = obs.mid_price();
85
86 let mut used_real_vol = false;
88 if self.params.allow_real_vol
89 && let Some(vol) = obs.volatility
90 {
91 self.sigma = vol * self.params.dt.sqrt();
93 used_real_vol = true;
94 }
95
96 if !used_real_vol && self.params.enable_vol_estimation {
97 self.update_volatility(mid_price);
98 }
99
100 let position = obs.portfolio.position;
102
103 let elapsed_seconds = obs.timestamp - self.start_time.unwrap();
105
106 let time_remaining = self.params.t_horizon - elapsed_seconds;
107
108 if time_remaining <= 0.0 {
110 requests.push(OrderRequest::CancelAll);
111 return;
112 }
113
114 let mut reservation_price =
116 mid_price - position * self.params.gamma * self.sigma.powi(2) * time_remaining;
117
118 if self.params.impact_parameter.abs() > f64::EPSILON {
120 reservation_price -= position * self.params.impact_parameter;
121 }
122
123 let half_spread = (self.params.gamma * self.sigma.powi(2) * time_remaining
125 + (2.0 / self.params.gamma) * (1.0 + self.params.gamma / self.params.k).ln())
126 / 2.0;
127
128 let bid_price = ((reservation_price - half_spread) * 100.0).round() / 100.0;
129 let ask_price = ((reservation_price + half_spread) * 100.0).round() / 100.0;
130
131 let quantity = 1.0;
132
133 self.order_counter += 1;
134 let bid_id = self.order_counter;
135 self.order_counter += 1;
136 let ask_id = self.order_counter;
137
138 requests.push(OrderRequest::CancelAll);
139 requests.push(OrderRequest::New(Order::new(
140 bid_id,
141 Side::Buy,
142 bid_price,
143 quantity,
144 )));
145 requests.push(OrderRequest::New(Order::new(
146 ask_id,
147 Side::Sell,
148 ask_price,
149 quantity,
150 )));
151 }
152
153 fn as_any(&self) -> &dyn std::any::Any {
154 self
155 }
156 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
157 self
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use crate::types::PortfolioSnapshot;
165
166 fn make_obs(bid: f64, ask: f64, position: f64, vol: Option<f64>) -> Observation {
167 Observation {
168 timestamp: 0.0,
169 best_bid: bid,
170 best_ask: ask,
171 last_price: Some((bid + ask) / 2.0),
172 portfolio: PortfolioSnapshot {
173 cash: 10000.0,
174 position,
175 },
176 volatility: vol,
177 drift: None,
178 parameters: None,
179 }
180 }
181
182 #[test]
183 fn test_places_symmetric_quotes_at_zero_inventory() {
184 let params = AvellanedaStoikovParameters {
185 gamma: 0.1,
186 sigma: 0.001,
187 t_horizon: 1000.0,
188 k: 1.5,
189 a: 140.0,
190 dt: 1.0,
191 allow_real_vol: false,
192 enable_vol_estimation: false,
193 impact_parameter: 0.0,
194 };
195 let mut strategy = AvellanedaStoikovStrategy::new(params);
196 let obs = make_obs(99.95, 100.05, 0.0, None);
197
198 let mut requests = Vec::new();
199 strategy.on_tick(&obs, &mut requests);
200
201 assert_eq!(requests.len(), 3);
203 let bid_price = match &requests[1] {
205 OrderRequest::New(o) => {
206 assert_eq!(o.side, Side::Buy);
207 o.limit_price
208 }
209 _ => panic!("Expected New order"),
210 };
211 let ask_price = match &requests[2] {
212 OrderRequest::New(o) => {
213 assert_eq!(o.side, Side::Sell);
214 o.limit_price
215 }
216 _ => panic!("Expected New order"),
217 };
218 let mid = 100.0;
219 assert!((mid - bid_price - (ask_price - mid)).abs() < 0.02);
221 }
222
223 #[test]
224 fn test_skews_quotes_with_positive_inventory() {
225 let params = AvellanedaStoikovParameters {
226 gamma: 0.1,
227 sigma: 0.001,
228 t_horizon: 1000.0,
229 k: 1.5,
230 a: 140.0,
231 dt: 1.0,
232 allow_real_vol: false,
233 enable_vol_estimation: false,
234 impact_parameter: 0.0,
235 };
236 let mut strategy = AvellanedaStoikovStrategy::new(params);
237 let obs = make_obs(99.95, 100.05, 5.0, None);
239
240 let mut requests = Vec::new();
241 strategy.on_tick(&obs, &mut requests);
242
243 let bid_price = match &requests[1] {
244 OrderRequest::New(o) => o.limit_price,
245 _ => panic!("Expected New"),
246 };
247 let ask_price = match &requests[2] {
248 OrderRequest::New(o) => o.limit_price,
249 _ => panic!("Expected New"),
250 };
251 let mid = 100.0;
252 let reservation = (bid_price + ask_price) / 2.0;
253 assert!(reservation < mid);
255 }
256
257 #[test]
258 fn test_uses_observed_volatility_when_allowed() {
259 let params = AvellanedaStoikovParameters {
260 gamma: 0.1,
261 sigma: 0.0001, t_horizon: 1000.0,
263 k: 1.5,
264 a: 140.0,
265 dt: 1.0,
266 allow_real_vol: true,
267 enable_vol_estimation: false,
268 impact_parameter: 0.0,
269 };
270 let mut strategy = AvellanedaStoikovStrategy::new(params);
271 let obs = make_obs(99.95, 100.05, 0.0, Some(0.5)); let mut requests = Vec::new();
273 strategy.on_tick(&obs, &mut requests);
274
275 assert!((strategy.sigma - 0.5).abs() < 1e-6); }
278
279 #[test]
280 fn test_cancels_all_when_time_is_up() {
281 let params = AvellanedaStoikovParameters {
282 gamma: 0.1,
283 sigma: 0.001,
284 t_horizon: 0.0, k: 1.5,
286 a: 140.0,
287 dt: 1.0,
288 allow_real_vol: false,
289 enable_vol_estimation: false,
290 impact_parameter: 0.0,
291 };
292 let mut strategy = AvellanedaStoikovStrategy::new(params);
293 let obs = make_obs(99.95, 100.05, 0.0, None);
294 let mut requests = Vec::new();
295 strategy.on_tick(&obs, &mut requests);
296
297 assert_eq!(requests.len(), 1);
298 assert!(matches!(requests[0], OrderRequest::CancelAll));
299 }
300}