1use super::Strategy;
2use crate::types::{Observation, Order, OrderRequest, Side};
3use std::collections::VecDeque;
4
5#[derive(Clone, Debug)]
10pub struct KellyParameters {
11 pub gamma: f64,
13 pub k: f64,
15 pub a: f64,
17 pub t_horizon: f64,
19 pub base_half_spread: f64,
22 pub max_position: f64,
25 pub min_sigma: f64,
27}
28
29pub struct KellyStrategy {
41 pub params: KellyParameters,
42 price_history: VecDeque<f64>,
43 window_size: usize,
44 mu_est: f64,
45 sigma_est: f64,
46 start_time: Option<f64>,
47 order_counter: u64,
48}
49
50impl KellyStrategy {
51 pub fn new(params: KellyParameters) -> Self {
52 Self {
53 params,
54 price_history: VecDeque::new(),
55 window_size: 100,
56 mu_est: 0.0,
57 sigma_est: 0.0,
58 start_time: None,
59 order_counter: 0,
60 }
61 }
62
63 pub fn with_window_size(mut self, window_size: usize) -> Self {
64 self.window_size = window_size;
65 self
66 }
67
68 fn update_estimates(&mut self, price: f64) {
69 self.price_history.push_back(price);
70 if self.price_history.len() > self.window_size {
71 self.price_history.pop_front();
72 }
73
74 if self.price_history.len() < 3 {
75 return;
76 }
77
78 let mut log_returns = Vec::with_capacity(self.price_history.len() - 1);
79 for i in 0..self.price_history.len() - 1 {
80 let r = (self.price_history[i + 1] / self.price_history[i]).ln();
81 log_returns.push(r);
82 }
83
84 let mean = log_returns.iter().sum::<f64>() / log_returns.len() as f64;
85 let variance = log_returns.iter().map(|r| (r - mean).powi(2)).sum::<f64>()
86 / (log_returns.len() - 1) as f64;
87 let sigma_step = variance.sqrt();
88
89 let alpha = 0.1;
90 self.mu_est = (1.0 - alpha) * self.mu_est + alpha * mean;
91 self.sigma_est = (1.0 - alpha) * self.sigma_est + alpha * sigma_step;
92 }
93
94 fn target_inventory(&self) -> f64 {
95 let sigma = self.sigma_est.max(self.params.min_sigma);
96 let target = self.mu_est / (self.params.gamma * sigma * sigma);
97 target.clamp(-self.params.max_position, self.params.max_position)
98 }
99}
100
101impl Strategy for KellyStrategy {
102 fn on_tick(&mut self, obs: &Observation, requests: &mut Vec<OrderRequest>) {
103 if self.start_time.is_none() {
104 self.start_time = Some(obs.timestamp);
105 }
106
107 let elapsed = obs.timestamp - self.start_time.unwrap();
108 let time_remaining = (self.params.t_horizon - elapsed).max(0.0);
109
110 if time_remaining <= 0.0 {
111 requests.push(OrderRequest::CancelAll);
112 return;
113 }
114
115 let mid = obs.mid_price();
116 self.update_estimates(mid);
117
118 let q = obs.portfolio.position;
119 let q_target = self.target_inventory();
120 let sigma = self.sigma_est.max(self.params.min_sigma);
121
122 let reservation_price =
125 mid - self.params.gamma * sigma.powi(2) * time_remaining * (q - q_target);
126
127 let spread_adjustment = self.params.gamma * sigma.powi(2) * time_remaining;
131 let half_spread = (spread_adjustment + 2.0 * self.params.base_half_spread) / 2.0;
132
133 let bid_price = reservation_price - half_spread;
134 let ask_price = reservation_price + half_spread;
135
136 self.order_counter += 1;
137 let bid_id = self.order_counter;
138 self.order_counter += 1;
139 let ask_id = self.order_counter;
140
141 requests.push(OrderRequest::CancelAll);
142 requests.push(OrderRequest::New(Order::new(
143 bid_id,
144 Side::Buy,
145 bid_price,
146 1.0,
147 )));
148 requests.push(OrderRequest::New(Order::new(
149 ask_id,
150 Side::Sell,
151 ask_price,
152 1.0,
153 )));
154 }
155
156 fn as_any(&self) -> &dyn std::any::Any {
157 self
158 }
159 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
160 self
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use crate::types::PortfolioSnapshot;
168
169 fn make_obs(timestamp: f64, mid: f64, position: f64) -> Observation {
170 Observation {
171 timestamp,
172 best_bid: mid - 0.05,
173 best_ask: mid + 0.05,
174 last_price: Some(mid),
175 portfolio: PortfolioSnapshot {
176 cash: 10000.0,
177 position,
178 },
179 volatility: None,
180 drift: None,
181 parameters: None,
182 }
183 }
184
185 fn default_params() -> KellyParameters {
186 KellyParameters {
187 gamma: 0.1,
188 k: 1.5,
189 a: 140.0,
190 t_horizon: 1000.0,
191 base_half_spread: 0.05,
192 max_position: 10.0,
193 min_sigma: 1e-6,
194 }
195 }
196
197 #[test]
198 fn test_happy_path_places_orders() {
199 let mut strategy = KellyStrategy::new(default_params());
200 let obs = make_obs(0.0, 100.0, 0.0);
201 let mut requests = Vec::new();
202 strategy.on_tick(&obs, &mut requests);
203
204 assert_eq!(requests.len(), 3);
205 assert!(matches!(requests[0], OrderRequest::CancelAll));
206 let bid = match &requests[1] {
208 OrderRequest::New(o) => {
209 assert_eq!(o.side, Side::Buy);
210 o.limit_price
211 }
212 _ => panic!("Expected New bid"),
213 };
214 let ask = match &requests[2] {
215 OrderRequest::New(o) => {
216 assert_eq!(o.side, Side::Sell);
217 o.limit_price
218 }
219 _ => panic!("Expected New ask"),
220 };
221 assert!(bid < ask, "bid={bid} must be below ask={ask}");
222 }
223
224 #[test]
225 fn test_zero_drift_gives_symmetric_quotes() {
226 let mut strategy = KellyStrategy::new(default_params());
228 for i in 0..20 {
229 let obs = make_obs(i as f64, 100.0, 0.0);
230 let mut requests = Vec::new();
231 strategy.on_tick(&obs, &mut requests);
232 }
233
234 let target = strategy.target_inventory();
236 assert!(target.abs() < 1e-6);
237 }
238
239 #[test]
240 fn test_positive_drift_targets_long_position() {
241 let mut strategy = KellyStrategy::new(default_params());
242 for i in 0..100 {
244 let price = 100.0 * 1.001_f64.powi(i);
245 let obs = make_obs(i as f64, price, 0.0);
246 let mut requests = Vec::new();
247 strategy.on_tick(&obs, &mut requests);
248 }
249
250 assert!(strategy.mu_est > 0.0, "Expected positive drift estimate");
252 let target = strategy.target_inventory();
253 assert!(target > 0.0, "Expected positive target, got {target}");
254 }
255
256 #[test]
257 fn test_negative_drift_targets_short_position() {
258 let mut strategy = KellyStrategy::new(default_params());
259 for i in 0..100 {
261 let price = 100.0 * 0.999_f64.powi(i);
262 let obs = make_obs(i as f64, price, 0.0);
263 let mut requests = Vec::new();
264 strategy.on_tick(&obs, &mut requests);
265 }
266
267 assert!(strategy.mu_est < 0.0, "Expected negative drift estimate");
269 let target = strategy.target_inventory();
270 assert!(target < 0.0, "Expected negative target, got {target}");
271 }
272
273 #[test]
274 fn test_target_clamped_to_max_position() {
275 let mut strategy = KellyStrategy::new(default_params());
276 for i in 0..100 {
278 let price = 100.0 * 1.01_f64.powi(i);
279 let obs = make_obs(i as f64, price, 0.0);
280 let mut requests = Vec::new();
281 strategy.on_tick(&obs, &mut requests);
282 }
283
284 let target = strategy.target_inventory();
285 assert!(
286 target.abs() <= strategy.params.max_position + 1e-9,
287 "target={target} exceeds max_position={}",
288 strategy.params.max_position
289 );
290 }
291
292 #[test]
293 fn test_cancels_all_when_horizon_elapsed() {
294 let mut params = default_params();
295 params.t_horizon = 0.0;
296 let mut strategy = KellyStrategy::new(params);
297 let obs = make_obs(0.0, 100.0, 0.0);
298 let mut requests = Vec::new();
299 strategy.on_tick(&obs, &mut requests);
300
301 assert_eq!(requests.len(), 1);
302 assert!(matches!(requests[0], OrderRequest::CancelAll));
303 }
304}